8

Java中,那些关于String和字符串常量池你不得不知道的东西

 3 years ago
source link: https://segmentfault.com/a/1190000039074103
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

老套的笔试题

在一些老套的笔试题中,会要你判断s1==s2为false还是true,s1.equals(s2)为false还是true。

String s1 = new String("xyz");
String s2 = "xyz";
System.out.println(s1 == s2);
System.out.println(s1.equals(s2));

对于这种题,你总能很快的给出标准答案:==比较的是对象地址,equals方法比较的是真正的字符数组。所以输出的是false和true。

上面的属于最低阶的题目,没有什么难度。

现在这种老套的题目已经慢慢消失了,取而代之的是有一些变形的新题目:

String s1 = "aa";
String s2 = "bb";
String str1 = s1 + s2;
String str2 = "aabb";
//输出什么呢???
System.out.println(str1 == str2);

final String s3 = "cc";
final String s4 = "dd";
String str3 = s3 + s4;
String str4 = "ccdd";
//又输出什么呢???
System.out.println(str3 == str4);

难度提升了一些,但思考一下也不难得出答案是false和true。

今天的文章就是以这几个题目展开的。

String对象的创建

先简单看一下String类的结构:

fe6zAvv.png!mobile

可以发现,String里面有一个value属性,是真正存储字符的char数组。

在执行 String s = "xyz"; 的时候,在堆区创建了一个String对象,一个char数组对象。

riEJZz7.png!mobile

如何证明创建了一个String对象和一个char数组对象呢?我们可以通过IDEA的Debug功能验证:

nm6JJfb.gif!mobile

注意看我截图的位置,在执行完 String s = "xyz"; 之后,再次点击load classes,Diff栏的String和char[]分别加了1,表示在内存中新增了一个char数组对象和一个String对象。

现在,我们再来看 String s = new String("xyz"); 创建了几个对象。

YBVbMn7.gif!mobile

从这张Debug动图中,我们可以得出在 String s = new String("xyz"); 之后,创建了两个String对象和一个char数组对象。

又因为 String s = new String("xyz");s 引用只能指向一个对象,可以画出内存分布图:

zeEFBfQ.png!mobile

从图中可以看到,在堆区,有两个String对象,这两个String对象的value都指向同一个char数组对象。

那么问题来了,下面的那个String对象根本就没被引用,也就是说他没有被用到,那么它到底是干什么的呢?

占了内存空间又不使用,难道这是JDK的设计缺陷?

mmEV7zM.png!mobile

很显然不是JDK的缺陷,JDK虽然确实有设计缺陷,但不至于这么明显,这么愚蠢。

那下面的那个String对象是干什么的呢?

答案是用于驻留到字符串常量池中去的,注意,这里我用了一个 驻留 ,并不是直接把对象放到字符串常量池里面去,有什么区别我们后面再讲。

这里出现了 字符串常量池 的概念,我在 String s = new String("xyz")创建了几个实例你真的能答对吗? 中也有过比较详细的介绍,有兴趣的可以去看一下,这里不再重复了。

你只需要知道,字符串常量池在JVM源码中对应的类是StringTable,底层实现是一个Hashtable。

I7NZFba.png!mobile

我们以 String s = new String("xyz"); 为例:

首先去找字符串常量池找,看能不能找到“xyz”字符串对应对象的引用,如果字符串常量池中找不到:

  • 创建一个String对象和char数组对象
  • 将创建的String对象封装成HashtableEntry,作为StringTable的value进行存储
  • new String("xyz")会在堆区又创建一个String对象,char数组直接指向创建好的char数组对象

如果字符串常量池中能找到:

  • new String("xyz")会在堆区创建一个对象,char数组直接指向已经存在的char数组对象

YVZVbq2.png!mobile

String s = "xyz"; 是怎么样的逻辑:

首先去找字符串常量池找,看能不能找到“xyz”字符串的引用,如果字符串常量池中能找不到:

  • 创建一个String对象和char数组对象
  • 将创建的String对象封装成HashtableEntry,作为StringTable的value进行存储
  • 返回创建的String对象

如果字符串常量池中能找到:

  • 直接返回找到引用对应的String对象

6vUZ3er.png!mobile

总结而言就是:

对于 String s = new String("xyz"); 这种形式创建字符串对象,如果字符串常量池中能找到,创建一个String对象;如果如果字符串常量池中找不到,创建两个String对象。

对于 String s = "xyz"; 这种形式创建字符串对象,如果字符串常量池中能找到,不会创建String对象;如果如果字符串常量池中找不到,创建一个String对象。

yuyiuqu.png!mobile

所以,在日常开发中,能用 String s = "xyz"; 尽量不用 String s = new String("xyz"); ,因为可以少创建一个对象,节省一部分空间。

需要强调的是,字符串常量池存的不是字符串也不是String对象,而是一个个HashtableEntry,HashtableEntry里面的value指向的才是String对象,为了不让表述变得复杂,我省略了HashtableEntry的存在,但不代表它就不存在。

上文提到的驻留就是新建HashtableEntry指向String对象,并把HashtableEntry存入字符串常量池的过程。

在网上一些文章中,一些作者可能是为了让读者更好的理解,省略了一些这些,一定要注意辨别区分。

uEBZvyF.png!mobile

达成以上共识之后,我们再回顾一下那个老套的笔试题。

String s1 = new String("xyz");
String s2 = "xyz";
//为什么输出的是false呢?
System.out.println(s1 == s2);
//为什么输出的是true呢?
System.out.println(s1.equals(s2));

有了上面的基础之后,我们画出对应的内存图,s1 == s2为什么是false就一目了然了。

nEjaArE.png!mobile

因为equals方法比较的真正的char数据,而s1和s2最终指向的都是同一个char数组对象,所以s1.equals(s2)等于true。

关于他们最终指向的都是同一个char数组对象这一观点,也可以通过反射证明:

6rQrAr.png!mobile

我修改了str1指向的String对象的value,str2指向的对象也被影响了。

7jM3aer.png!mobile

字符串拼接

现在,我们再来看一下变式题:

String s1 = "aa";
String s2 = "bb";
String str1 = s1 + s2;
String str2 = "aabb";
//为什么输出的是false
System.out.println(str1 == str2);

对于这个题目,我们需要先看一下这段代码的字节码。

rqEbUf7.png!mobile

字节码指令看不懂没有关系,看我用红色框框起来的部分就行了,可以看到居然出现了StringBuilder。

什么意思呢,就是说 String str1 = s1 + s2; 会被编译器会优化成 new StringBuilder().append("aa").append("bb").toString();

StringBuilder里面的append方法就是对char数组进行操作,那StringBuilder的toString方法做了什么呢?

reaMZ3I.png!mobile

从源码中可以看到,StringBuilder里面的toString方法调用的是String类里面的 String(char value[], int offset, int count) 构造方法,这个方法做了什么呢?

  • 根据参数复制一份char数组对象。复制了一份!
  • 创建一个String对象,String对象的value指向复制的char数组对象。

注意,并没有驻留到字符串常量池里面去,这个很关键!!!画一个图理解一下:

ZzQNJzy.png!mobile

也就是说str2指向的String对象并没有驻留到字符串常量池,而str1指向的对象驻留到字符串常量池里面去了,且他们并不是同一个对象。所以str1 == str2还是false

因为复制一份char数组对象,所以如果我们改变其中一个char数组的话,另一个也不会造成影响:

vQNbyq.png!mobile

把其中String变成丑比之后,另一个还是帅比,也说明了两个String对象用的不是同一份char数组。

n2UbIff.gif!mobile

intern方法

上面说到,调用StringBuilder的toString方法创建的String对象是不会驻留到字符串常量池的,那如果我偏要驻留到字符串常量池呢?有没有办法呢?

有的,String类的intern方法就可以帮你完成这个事情。

以这段代码为例:

String s1 = "aa";
String s2 = "bb";
String str = s1 + s2;
str.intern();

在执行 str.intern(); 之前,内存图是这样的:

FFFzqa7.png!mobile

在执行 str.intern(); 之后,内存图是这样的:

eueeemQ.png!mobile

intern方法就是创建了一个HashtableEntry对象,并把value指向String对象,然后把HashtableEntry通过hash定位存到对应的字符串成常量池中。当然,前提是字符串常量池中原来没有对应的HashtableEntry。

没了,intern方法,就是这么简单,一句话给你说清楚了。

关于intern方法,还有一个很有趣的故事,有兴趣的可以去看一下why神的这篇文章 《深入理解Java虚拟机》第2版挖的坑终于在第3版中被R大填平了

编译优化

写到这里,好像只有一个坑没有填。就是这个题为什么输出的是true。

final String s3 = "cc";
final String s4 = "dd";
String str3 = s3 + s4;
String str4 = "ccdd";
//为什么输出的是true呢???
System.out.println(str3 == str4);

这道题和上面那道题相比,有点相似,在原来的基础上加了两个final关键字。我们先看一下这段代码的字节码:

nmIJN32.png!mobile

fEbARbZ.png!mobile

又是一段字节码指令,不需要看懂,你点一下#4,居然就可以看到“ccdd”字符串。

原来,用final修饰后,JDK的编译器会识别优化,会把 String str3 = s3 + s4; 优化成 String str3 = "ccdd"

aQBZjyU.png!mobile

所以原题就相当于:

String str3 = "ccdd";
String str4 = "ccdd";
//为什么输出的是true呢???
System.out.println(str3 == str4);

这样的题目还难吗?是不是那不管str3和str4怎么比,肯定是相等的。

总结

String对于Java程序员来说就是“最熟悉的陌生人”,你说String简单,它确实简单。你说它难,深究起来确实也有难度,但这些题目,只要你脑海里有一副内存图就会很简单。

面试题也只会越来越难,这个行业看起来也越来越内卷,但只要我学的快,内卷就卷不到我。

好了,今天就写到了,我要去打游戏了。

希望这篇文章,能对你有一点帮助。

写在最后(求关注)

我对每一篇发出去的文章负责,文中涉及知识理论,我都会尽量在官方文档和权威书籍找到并加以验证。但即使这样,我也不能保证文章中每个点都是正确的,如果你发现错误之处,欢迎指出,我会对其修正。

创作不易,为了更好的表达,需要画很多图,这些都是我自己动手用PPT画的,画图也很辛苦的!

YJveq23.png!mobile

所以,不要犹豫了,给点正反馈,答应我,一键三连(关注、点赞、再看)好吗?

我是CoderW,一个程序员。

谢谢你的阅读,我们下期再见!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK