Java字符串常量池详解(StringTable)

前言:在介绍字符串常量池之前,我们先来简单了解下Java中字符串的概念以及常见的一些问题.

参考书籍: 《Java核心技术》

参考网站:牛客

作者水平很有限,如果发现错误,麻烦及时告知作者哦!十分感谢!

目录

一、字符串

1.1 子串

1.2 拼接

1.2.1 字符串与字符串拼接

1.2.2 字符串与非字符串拼接

1.3 字符串的不可变性

1.3.1为什么设计成不可变的 

1.4 判断字符串是否相等

二、字符串常量池

2.1 字符串常量池的应用

2.1.1 再谈String对象创建

 三、面试题

一、字符串

Java字符串其实只是Unicode字符序列,Java本身没有内置的字符串类型,而是在标准Java类库中提供了一个预定类,很自然的叫做String,每一个双引号引起来的字符串都是String类的一个实例:

        String s = "";//空的字符串
        String str = "Hello";

1.1 子串

提到子串,那就免不了介绍以下subString这个方法了,该方法的功能为:从一个较大的字符串中提取一个子串。

其一为,subString(x,y):(注意这里是左闭右开区间) 所以该方法是截取下标x~y-1 的字符串。

其二为,subString(x) : 该方法是截去前x个字符串后的字符串。

具体情况如下代码:

Java字符串常量池详解(StringTable)_第1张图片

ps:subString 的工作方式有一个优点,容易计算子串的长度,比如:字符串s.subString(a,b) 的长度就为b-a。 

1.2 拼接

1.2.1 字符串与字符串拼接

与绝大多数设计语言一样,Java语言允许使用+号来连接两个字符串。

下述代码将“hello”赋值给了s3变量:

Java字符串常量池详解(StringTable)_第2张图片

1.2.2 字符串与非字符串拼接

当将一个字符串与非字符串的值进行拼接的时候,后者会转换成字符串,例如:

Java字符串常量池详解(StringTable)_第3张图片

 此特性其实我们经常在打印时候用到:

Java字符串常量池详解(StringTable)_第4张图片

注意:这里是字符串与非字符拼接并不是字符与非字符拼接:

如果是后者的话;可能会转变成数字,例:

Java字符串常量池详解(StringTable)_第5张图片

1.3 字符串的不可变性

String类中没有提供修改字符串中某个字符的方法,如果希望将”hello“修改为“help!” ,只能提取想要保存的字符串,再与希望替换的字符串拼接,比如:

Java字符串常量池详解(StringTable)_第6张图片

分析:

由于不能修改Java字符串中的某个字符,所以Java我、文档中将String类称之为不可变的,不过可以修改s指向的值,让它引用另外的字符串。 

可能有人会说,拼接字符串这样的做法是否会降低运行效率呢?

答案其实并不确定,虽然通过拼接的方法,让s2指向了"help!"看上去有些低效,但是不可变字符串却有一个优点:编译器可以让字符串共享。

原因:

Java字符串常量池详解(StringTable)_第7张图片

 通过观察源码可知,String类的value是被private修饰的,类外是拿不到这个值的

 可能很多人以为String类不可变是因为value[] 这个数组被final修饰,但其实并不是这样的,因为value存储的是一个引用,而不是常量。

下面用代码诠释一下:

Java字符串常量池详解(StringTable)_第8张图片

我们发现,arr[1]的值的确被改为了c。准确的说 这里的final限制的是arr指向的值不能改变,也就是不能重新指向一个新的数组对象。

Java字符串常量池详解(StringTable)_第9张图片 总结:

  1. String类被final修饰,表明该类不能被继承
  2. value被修饰被final修饰,表明value自身的值不能改变,即不能引用其它字符数组,但是其引用空间中的内容可以修改。

所以一些设计字符串修改的函数,都是创建一个新的对象,然后修改新的对象。 

Java字符串常量池详解(StringTable)_第10张图片

注意  += 这种的拼接操作,也不是在String类自身上进行的,而是通过中间创建许多临时变量而完成的,效率很低下。于是引入了StringBuffer和StringBuilder。

 以下代码可以看出String类在拼接时的效率比StringBuffer和StringBuilder低:

public static void main(String[] args) {
        long start = System.currentTimeMillis();
        String s = "";
        for(int i = 0; i < 1_0000; ++i){
            s += i;
        }
        long end = System.currentTimeMillis();
        System.out.println(end - start);
        start = System.currentTimeMillis();
        StringBuffer sbf = new StringBuffer("");
        for(int i = 0; i < 1_0000; ++i){
            sbf.append(i);
        }
        end = System.currentTimeMillis();
        System.out.println(end - start);
        start = System.currentTimeMillis();
        StringBuilder sbd = new StringBuilder();
        for(int i = 0; i < 1_0000; ++i){
            sbd.append(i);
        }
        end = System.currentTimeMillis();
        System.out.println(end - start);
    }

Java字符串常量池详解(StringTable)_第11张图片

1.3.1为什么设计成不可变的 

为什么 String 要涉及成不可变的?(不可变对象的好处是什么?) 

  1. 方便实现字符串对象池. 如果 String 可变, 那么对象池就需要考虑写时拷贝的问题了.
  2. 不可变对象是线程安全的.
  3. 不可变对象更方便缓存 hash code, 作为 key 时可以更高效的保存到 HashMap 中.

1.4 判断字符串是否相等

可以使用equals方法来检测字符串是否相等,形如:s.equals(r); 如果字符串s与字符串r相等,则返回true,否则返回false。

需要注意的是:s与t可以是字符串常量,也可以是字符串字面量(用双引号引起来的部分)。

比如以下的语句也是合法的:

Java字符串常量池详解(StringTable)_第12张图片

千万不要使用 == 来判断两个字符串是否相等,这个运算符只能确认两个字符串是否处在同一位置,虽然,字符串在同一位置上,它们必然相等, 但是完全可能将内容相同的多个字符串放在不同的位置上。

例如:

Java字符串常量池详解(StringTable)_第13张图片

如果虚拟机始终将相同的字符串共享,就可以使用 == 运算符来检测是否相等,但实际上字符串字面量是共享的,而+ 或 substring 等操作得到的字符串并不共享,因此千万不能使用==来判断字符串的相等性,以免出现bug。 

二、字符串常量池

字符串常量池在JVM中是StringTable类,实际是一个固定大小的HashTable(一种高效用来进行查找的数据结构,后序给大家详细介绍),不同JDK版本下字符串常量池的位置以及默认大小是不同的:

JDK版本 字符串常量池位置 大小设置
Java6 (方法区)永久代 固定大小:1009
Java7 堆中 可设置,没有大小限制,默认大小:60013
Java8 堆中 可设置,有范围限制,最小是1009

2.1 字符串常量池的应用

2.1.1 再谈String对象创建

Java字符串常量池详解(StringTable)_第14张图片

 大家可能会好奇,为什么s1,s2指向的是同一个对象,而s3,s4指向的对象却不相同呢?

在Java程序中,类似于:1, 2, 3,3.14,“hello”等字面类型的常量经常频繁使用,为了使程序的运行速度更快、更节省内存,提供了一个String类型的常量池,只要是双引号引起来的对象,都会存放在常量池中。

Java字符串常量池详解(StringTable)_第15张图片

 分析:因为s1,s2分别都是被双引号引起来的对象,所以都会被存储到常量池中,可能有人会问,为什么s2并没有单独在常量池中存储一份而是引用了s1所指向的对象呢?

; 那是因为被引号引起来的对象,会先到常量池中寻找是否已经存在相同的内容,如果存在相同内容,那么就直接引用它,就不需要再创建一个对象了(这也是为什么常量池效率高的原因)。

现在我们再来看几组代码;

代码1;

Java字符串常量池详解(StringTable)_第16张图片

 我们可以发现,s1,s2,s3都引用了同一个数组对象,这里s3不与s1,s2相同的原因是因为,s3是通过new了一个String对象,而String对象里面的value值存储的是hello的地址值。

Java字符串常量池详解(StringTable)_第17张图片

 代码2:

Java字符串常量池详解(StringTable)_第18张图片

Java字符串常量池详解(StringTable)_第19张图片

 下面难度加大一点:

Java字符串常量池详解(StringTable)_第20张图片

小插曲:

这里需要注意的是s1的new string对象中存放的是数组,根据源码我们可以得知,是将这个ch数组拷贝一份再存入string对象中的。

Java字符串常量池详解(StringTable)_第21张图片

 但是我们只要加入intern这个方法,就可以让其相等。

原因:intern 是一个native方法(Native方法指,底层使用C++实现的,看不到其实现的源代码)
:该方法会检查常量池中是否有这个对象,如果无,则入池(入池并不会将自身销毁,而是在常量池中添加这个对象),有的话,则返回该对象(并不会再次入池了)。
Java字符串常量池详解(StringTable)_第22张图片

如果将这个顺序一调换,就输出false。

原因也很简单,因为已经在常量池中存在这个”abc“了,不会再将s1的string对象的abc再次入池。

Java字符串常量池详解(StringTable)_第23张图片

 三、面试题

 面试题:请解释String类中两种对象实例化的区别
JDK1.8中(该题的前提是:常量池不存在hello)
1. String str = "hello"
只会开辟一块堆内存空间,保存在字符串常量池中,然后str共享常量池中的String对象
2. String str = new String("hello")
会开辟两块堆内存空间,字符串"hello"保存在字符串常量池中,然后用常量池中的String对象给新开辟
的String对象赋值。
3. String str = new String(new char[]{'h', 'e', 'l', 'l', 'o'})
现在堆上创建一个String对象,然后利用copyof将重新开辟数组空间,将参数字符串数组中内容拷贝到
String对象中

你可能感兴趣的:(jvm,java)