老套的口试题
在一些老套的口试题中,会要你判断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类的构造:
能够发现,String外面有一个value属性,是真正存储字符的char数组。
在执行String s = "xyz";
的时候,在堆区创立了一个String对象,一个char数组对象。
如何证实创立了一个String对象和一个char数组对象呢?咱们能够通过IDEA的Debug性能验证:
留神看我截图的地位,在执行完String s = "xyz";
之后,再次点击load classes,Diff栏的String和char[]别离加了1,示意在内存中新增了一个char数组对象和一个String对象。
当初,咱们再来看String s = new String("xyz");
创立了几个对象。
从这张Debug动图中,咱们能够得出在String s = new String("xyz");
之后,创立了两个String对象和一个char数组对象。
又因为String s = new String("xyz");
的s
援用只能指向一个对象,能够画出内存分布图:
从图中能够看到,在堆区,有两个String对象,这两个String对象的value都指向同一个char数组对象。
那么问题来了,上面的那个String对象基本就没被援用,也就是说他没有被用到,那么它到底是干什么的呢?
占了内存空间又不应用,难道这是JDK的设计缺点?
很显然不是JDK的缺点,JDK尽管的确有设计缺点,但不至于这么显著,这么愚昧。
那上面的那个String对象是干什么的呢?
答案是用于驻留到字符串常量池中去的,留神,这里我用了一个驻留
,并不是间接把对象放到字符串常量池外面去,有什么区别咱们前面再讲。
这里呈现了字符串常量池
的概念,我在String s = new String("xyz")创立了几个实例你真的能答对吗?中也有过比拟具体的介绍,有趣味的能够去看一下,这里不再反复了。
你只须要晓得,字符串常量池在JVM源码中对应的类是StringTable,底层实现是一个Hashtable。
那字符串到底是怎么存的呢?
咱们以String s = new String("xyz");
为例:
首先去找字符串常量池找,看能不能找到“xyz”字符串对应对象的援用,如果字符串常量池中找不到:
- 创立一个String对象和char数组对象
- 将创立的String对象封装成HashtableEntry,作为StringTable的value进行存储
- new String("xyz")会在堆区又创立一个String对象,char数组间接指向创立好的char数组对象
如果字符串常量池中能找到:
- new String("xyz")会在堆区创立一个对象,char数组间接指向曾经存在的char数组对象
而String s = "xyz";
是怎么样的逻辑:
首先去找字符串常量池找,看能不能找到“xyz”字符串的援用,如果字符串常量池中能找不到:
- 创立一个String对象和char数组对象
- 将创立的String对象封装成HashtableEntry,作为StringTable的value进行存储
- 返回创立的String对象
如果字符串常量池中能找到:
- 间接返回找到援用对应的String对象
总结而言就是:
对于String s = new String("xyz");
这种模式创立字符串对象,如果字符串常量池中能找到,创立一个String对象;如果如果字符串常量池中找不到,创立两个String对象。
对于String s = "xyz";
这种模式创立字符串对象,如果字符串常量池中能找到,不会创立String对象;如果如果字符串常量池中找不到,创立一个String对象。
所以,在日常开发中,能用String s = "xyz";
尽量不必String s = new String("xyz");
,因为能够少创立一个对象,节俭一部分空间。
须要强调的是,字符串常量池存的不是字符串也不是String对象,而是一个个HashtableEntry,HashtableEntry外面的value指向的才是String对象,为了不让表述变得复杂,我省略了HashtableEntry的存在,但不代表它就不存在。
上文提到的驻留就是新建HashtableEntry指向String对象,并把HashtableEntry存入字符串常量池的过程。
在网上一些文章中,一些作者可能是为了让读者更好的了解,省略了一些这些,肯定要留神分别辨别。
达成以上共识之后,咱们再回顾一下那个老套的口试题。
String s1 = new String("xyz");String s2 = "xyz";//为什么输入的是false呢?System.out.println(s1 == s2);//为什么输入的是true呢?System.out.println(s1.equals(s2));
有了下面的根底之后,咱们画出对应的内存图,s1 == s2为什么是false就高深莫测了。
因为equals办法比拟的真正的char数据,而s1和s2最终指向的都是同一个char数组对象,所以s1.equals(s2)等于true。
对于他们最终指向的都是同一个char数组对象这一观点,也能够通过反射证实:
我批改了str1指向的String对象的value,str2指向的对象也被影响了。
字符串拼接
当初,咱们再来看一下变式题:
String s1 = "aa";String s2 = "bb";String str1 = s1 + s2;String str2 = "aabb";//为什么输入的是falseSystem.out.println(str1 == str2);
对于这个题目,咱们须要先看一下这段代码的字节码。
字节码指令看不懂没有关系,看我用红色框框起来的局部就行了,能够看到竟然呈现了StringBuilder。
什么意思呢,就是说String str1 = s1 + s2;
会被编译器会优化成new StringBuilder().append("aa").append("bb").toString();
StringBuilder外面的append办法就是对char数组进行操作,那StringBuilder的toString办法做了什么呢?
从源码中能够看到,StringBuilder外面的toString办法调用的是String类外面的String(char value[], int offset, int count)
构造方法,这个办法做了什么呢?
- 依据参数复制一份char数组对象。复制了一份!
- 创立一个String对象,String对象的value指向复制的char数组对象。
留神,并没有驻留到字符串常量池外面去,这个很要害!!!画一个图了解一下:
也就是说str2指向的String对象并没有驻留到字符串常量池,而str1指向的对象驻留到字符串常量池外面去了,且他们并不是同一个对象。所以str1 == str2还是false
因为复制一份char数组对象,所以如果咱们扭转其中一个char数组的话,另一个也不会造成影响:
把其中String变成丑比之后,另一个还是帅比,也阐明了两个String对象用的不是同一份char数组。
intern办法
下面说到,调用StringBuilder的toString办法创立的String对象是不会驻留到字符串常量池的,那如果我偏要驻留到字符串常量池呢?有没有方法呢?
有的,String类的intern办法就能够帮你实现这个事件。
以这段代码为例:
String s1 = "aa";String s2 = "bb";String str = s1 + s2;str.intern();
在执行str.intern();
之前,内存图是这样的:
在执行str.intern();
之后,内存图是这样的:
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关键字。咱们先看一下这段代码的字节码:
又是一段字节码指令,不须要看懂,你点一下#4,竟然就能够看到“ccdd”字符串。
原来,用final润饰后,JDK的编译器会辨认优化,会把String str3 = s3 + s4;
优化成String str3 = "ccdd"
。
所以原题就相当于:
String str3 = "ccdd";String str4 = "ccdd";//为什么输入的是true呢???System.out.println(str3 == str4);
这样的题目还难吗?是不是那不论str3和str4怎么比,必定是相等的。
总结
String对于Java程序员来说就是“最相熟的陌生人”,你说String简略,它的确简略。你说它难,深究起来的确也有难度,但这些题目,只有你脑海里有一副内存图就会很简略。
面试题也只会越来越难,这个行业看起来也越来越内卷,但只有我学的快,内卷就卷不到我。
好了,明天就写到了,我要去打游戏了。
心愿这篇文章,能对你有一点帮忙。
写在最初
我对每一篇收回去的文章负责,文中波及常识实践,我都会尽量在官网文档和权威书籍找到并加以验证。但即便这样,我也不能保障文章中每个点都是正确的,如果你发现错误之处,欢送指出,我会对其修改。
创作不易,为了更好的表白,须要画很多图,这些都是我本人入手用PPT画的,画图也很辛苦的!
所以,不要犹豫了,给点正反馈,许可我,非常欢送并感激你的关注
我是CoderW,一个程序员。
谢谢你的浏览,咱们下期再见!