这是CoderW的第 9 篇原创文章
![](https://oscimg.oschina.net/oscnet/c3baee16-6f60-4362-9192-18152e51bdeb.jpg)
前言
前几天一个读者找到我,说我的文章写得好,这是第一次被读者加好友并给我正反馈。
![](https://oscimg.oschina.net/oscnet/3134966d-6bf5-4293-9c1d-9021f726223d.png)
怎么说呢,我没想到过自己写的东西居然能够帮助到别人,虽然可能只是是一句客套的夸赞,也许还含有很多的鼓励成分,但我还是挺开心的。
我自知水平还不太行,文章写得也不太好,平时朋友给我点赞大多也是鼓励一下,有些朋友甚至都不是程序员,看不懂还给我点赞,其实我心里都明白,真的非常感谢。
好了,矫情的话点到为止,继续搞技术。
前面提到的那位读者还和我探讨了一些技术问题,其中关于Java中String的创建逻辑读者不是很明白,我觉得我在String s = new String("xyz")创建了几个实例你真的能答对吗?那篇文章里面也说的确实不够详细,今天补充一下。
老套的笔试题
在一些老套的笔试题中,会要你判断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类的结构:
![](https://oscimg.oschina.net/oscnet/fd0283f6-9b0b-4593-a991-a9d51afb6dbc.png)
可以发现,String里面有一个value属性,是真正存储字符的char数组。
在执行String s = "xyz";
的时候,在堆区创建了一个String对象,一个char数组对象。
![](https://oscimg.oschina.net/oscnet/faa572db-f17c-41f6-9f83-478e638bb8e0.png)
如何证明创建了一个String对象和一个char数组对象呢?我们可以通过IDEA的Debug功能验证:
![](https://oscimg.oschina.net/oscnet/f6576a7a-5dd8-4f7f-b79a-5a8bc64a095f.gif)
注意看我截图的位置,在执行完String s = "xyz";
之后,再次点击load classes,Diff栏的String和char[]分别加了1,表示在内存中新增了一个char数组对象和一个String对象。
现在,我们再来看String s = new String("xyz");
创建了几个对象。
![](https://oscimg.oschina.net/oscnet/4ca6be32-b61c-461d-afda-faf801627e46.gif)
从这张Debug动图中,我们可以得出在String s = new String("xyz");
之后,创建了两个String对象和一个char数组对象。
又因为String s = new String("xyz");
的s
引用只能指向一个对象,可以画出内存分布图:
![](https://oscimg.oschina.net/oscnet/c99aa014-d04c-46df-8cee-d6115225e0f5.png)
从图中可以看到,在堆区,有两个String对象,这两个String对象的value都指向同一个char数组对象。
那么问题来了,下面的那个String对象根本就没被引用,也就是说他没有被用到,那么它到底是干什么的呢?
占了内存空间又不使用,难道这是JDK的设计缺陷?
![](https://oscimg.oschina.net/oscnet/d29e4a1c-706c-4465-9e1e-be911cd07961.png)
很显然不是JDK的缺陷,JDK虽然确实有设计缺陷,但不至于这么明显,这么愚蠢。
那下面的那个String对象是干什么的呢?
答案是用于驻留到字符串常量池中去的,注意,这里我用了一个驻留
,并不是直接把对象放到字符串常量池里面去,有什么区别我们后面再讲。
这里出现了字符串常量池
的概念,我在String s = new String("xyz")创建了几个实例你真的能答对吗?中也有过比较详细的介绍,有兴趣的可以去看一下,这里不再重复了。
你只需要知道,字符串常量池在JVM源码中对应的类是StringTable,底层实现是一个Hashtable。
![](https://oscimg.oschina.net/oscnet/60061980-29b2-441c-bb6c-7861607d8401.png)
我们以String s = new String("xyz");
为例:
首先去找字符串常量池找,看能不能找到“xyz”字符串对应对象的引用,如果字符串常量池中找不到:
-
创建一个String对象和char数组对象 -
将创建的String对象封装成HashtableEntry,作为StringTable的value进行存储 -
new String("xyz")会在堆区又创建一个String对象,char数组直接指向创建好的char数组对象
如果字符串常量池中能找到:
-
new String("xyz")会在堆区创建一个对象,char数组直接指向已经存在的char数组对象
![](https://oscimg.oschina.net/oscnet/d9831f29-6eac-4de6-8d4a-22ebc6d66694.png)
而String s = "xyz";
是怎么样的逻辑:
首先去找字符串常量池找,看能不能找到“xyz”字符串的引用,如果字符串常量池中能找不到:
-
创建一个String对象和char数组对象 -
将创建的String对象封装成HashtableEntry,作为StringTable的value进行存储 -
返回创建的String对象
如果字符串常量池中能找到:
-
直接返回找到引用对应的String对象
![](https://oscimg.oschina.net/oscnet/23c688a9-4a93-4e60-a563-8c8c6da707b1.png)
总结而言就是:
对于String s = new String("xyz");
这种形式创建字符串对象,如果字符串常量池中能找到,创建一个String对象;如果如果字符串常量池中找不到,创建两个String对象。
对于String s = "xyz";
这种形式创建字符串对象,如果字符串常量池中能找到,不会创建String对象;如果如果字符串常量池中找不到,创建一个String对象。
![](https://oscimg.oschina.net/oscnet/9e324381-066d-4ada-8635-85e86248be80.png)
所以,在日常开发中,能用String s = "xyz";
尽量不用String s = new String("xyz");
,因为可以少创建一个对象,节省一部分空间。
需要强调的是,字符串常量池存的不是字符串也不是String对象,而是一个个HashtableEntry,HashtableEntry里面的value指向的才是String对象,为了不让表述变得复杂,我省略了HashtableEntry的存在,但不代表它就不存在。
上文提到的驻留就是新建HashtableEntry指向String对象,并把HashtableEntry存入字符串常量池的过程。
在网上一些文章中,一些作者可能是为了让读者更好的理解,省略了一些这些,一定要注意辨别区分。
![](https://oscimg.oschina.net/oscnet/2f896ab6-56a7-4352-95de-80cb9cc7e959.png)
达成以上共识之后,我们再回顾一下那个老套的笔试题。
String s1 = new String("xyz");
String s2 = "xyz";
//为什么输出的是false呢?
System.out.println(s1 == s2);
//为什么输出的是true呢?
System.out.println(s1.equals(s2));
有了上面的基础之后,我们画出对应的内存图,s1 == s2为什么是false就一目了然了。
![](https://oscimg.oschina.net/oscnet/2fc117a2-e07b-4f48-aa67-e6e59d3a64d2.png)
因为equals方法比较的真正的char数组,而s1和s2最终指向的都是同一个char数组对象,所以s1.equals(s2)等于true。
关于他们最终指向的都是同一个char数组对象这一观点,也可以通过反射证明:
![](https://oscimg.oschina.net/oscnet/8c80ac87-3d2e-4375-99f6-857156c521b3.png)
我修改了str1指向的String对象的value,str2指向的对象也被影响了。
![](https://oscimg.oschina.net/oscnet/7a2a5e28-ac49-4790-a1bb-20eccdcdf93b.png)
字符串拼接
现在,我们再来看一下变式题:
String s1 = "aa";
String s2 = "bb";
String str1 = s1 + s2;
String str2 = "aabb";
//为什么输出的是false
System.out.println(str1 == str2);
对于这个题目,我们需要先看一下这段代码的字节码。
![](https://oscimg.oschina.net/oscnet/ea3d84a2-d756-4a4e-b8ea-d05c93b47bbf.png)
字节码指令看不懂没有关系,看我用红色框框起来的部分就行了,可以看到居然出现了StringBuilder。
什么意思呢,就是说String str1 = s1 + s2;
会被编译器会优化成new StringBuilder().append("aa").append("bb").toString();
StringBuilder里面的append方法就是对char数组进行操作,那StringBuilder的toString方法做了什么呢?
![](https://oscimg.oschina.net/oscnet/90a95591-4e9f-417a-8aec-3751c4f93bd9.png)
从源码中可以看到,StringBuilder里面的toString方法调用的是String类里面的String(char value[], int offset, int count)
构造方法,这个方法做了什么呢?
-
根据参数复制一份char数组对象。复制了一份! -
创建一个String对象,String对象的value指向复制的char数组对象。
注意,并没有驻留到字符串常量池里面去,这个很关键!!!画一个图理解一下:
![](https://oscimg.oschina.net/oscnet/9869a123-6ced-465d-abca-3278833e9d47.png)
也就是说str2指向的String对象并没有驻留到字符串常量池,而str1指向的对象驻留到字符串常量池里面去了,且他们并不是同一个对象。所以str1 == str2还是false
因为复制一份char数组对象,所以如果我们改变其中一个char数组的话,另一个也不会造成影响:
![](https://oscimg.oschina.net/oscnet/d0c2628a-23b6-4e37-9ead-3615f59ae5f3.png)
把其中String变成丑比之后,另一个还是帅比,也说明了两个String对象用的不是同一份char数组。
![](https://oscimg.oschina.net/oscnet/ec246d52-4e6d-4351-8f8d-5c6b5d37dc63.gif)
intern方法
上面说到,调用StringBuilder的toString方法创建的String对象是不会驻留到字符串常量池的,那如果我偏要驻留到字符串常量池呢?有没有办法呢?
有的,String类的intern方法就可以帮你完成这个事情。
以这段代码为例:
String s1 = "aa";
String s2 = "bb";
String str = s1 + s2;
str.intern();
在执行str.intern();
之前,内存图是这样的:
![](https://oscimg.oschina.net/oscnet/65c64bfe-2557-4db3-b9c7-f0d48dc733f6.png)
在执行str.intern();
之后,内存图是这样的:
![](https://oscimg.oschina.net/oscnet/d98c3066-538b-4c43-b69d-0b597a78848b.png)
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关键字。我们先看一下这段代码的字节码:
![](https://oscimg.oschina.net/oscnet/123b445d-65ad-4cf4-ad7a-62fadbe399fe.png)
![](https://oscimg.oschina.net/oscnet/cddfcd57-4eed-466f-a139-aa968daf7f4c.png)
又是一段字节码指令,不需要看懂,你点一下#4,居然就可以看到“ccdd”字符串。
原来,用final修饰后,JDK的编译器会识别优化,会把String str3 = s3 + s4;
优化成String str3 = "ccdd"
。
![](https://oscimg.oschina.net/oscnet/614ba651-aba7-45f5-a228-fe29f9ae0aae.png)
所以原题就相当于:
String str3 = "ccdd";
String str4 = "ccdd";
//为什么输出的是true呢???
System.out.println(str3 == str4);
这样的题目还难吗?是不是那不管str3和str4怎么比,肯定是相等的。
总结
String对于Java程序员来说就是“最熟悉的陌生人”,你说String简单,它确实简单。你说它难,深究起来确实也有难度,但这些题目,只要你脑海里有一副内存图就会很简单。
面试题也只会越来越难,这个行业看起来也越来越内卷,但只要我学的快,内卷就卷不到我。
好了,今天就写到了,我要去打游戏了。
希望这篇文章,能对你有一点帮助。
写在最后(求关注)
我对每一篇发出去的文章负责,文中涉及知识理论,我都会尽量在官方文档和权威书籍找到并加以验证。但即使这样,我也不能保证文章中每个点都是正确的,如果你发现错误之处,欢迎指出,我会对其修正。
创作不易,为了更好的表达,需要画很多图,这些都是我自己动手用PPT画的,画图也很辛苦的!
![](https://oscimg.oschina.net/oscnet/6b08e3ce-4730-4b84-b8d1-9718c84060cb.png)
所以,不要犹豫了,给点正反馈,答应我,一键三连(关注、点赞、再看)好吗?
我是CoderW,一个程序员。
谢谢你的阅读,我们下期再见!
![](https://oscimg.oschina.net/oscnet/711ec2f6-9413-4d06-b12f-65e01e8d651e.png)
本文分享自微信公众号 - CoderW(MHXJ_0810)。
如有侵权,请联系 [email protected] 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。