一道难倒大部分程序员的String面试题
孙玉超
2020-07-20 16:15:21
0 评论
1023 浏览
0 收藏
1 赞
我自以为对Java堆栈内存以及String类很熟悉,不料最近做了几道String面试题,有一道题做错了,而且看完解释之后依然是感觉不太能接受。一言不合仔细研究了一下,写出来记录一下。学海无涯!
废话不多说,直接上代码:
String s1 = new String("1") + new String("1"); s1.intern(); String s2 = "11"; System.out.println(s1 == s2);
呐,就是这道题目,有兴趣可以思考下。我的答案是 false ,但是运行答案是 true (JDK1.8)。而且我很疑惑,看了很多博客,最后也看了专业老师的讲解,总感觉还是难以接受。不过反推的话,好像呢,也只能那样解释。
那先说一下我自己的见解吧,第一行堆空间先产生两个 String 实例,然后 用 StringBuilder 连接,再返回新的 String 实例。这一步不会在常量池创建 "11" 字符串。
第二行调用 s1 的 intern() 方法,由于此时字符串常量池没有 "11" 字符串,所以会在常量池中创建字符串 "11" 。
第三行由于常量池已经存在字符串 "11",所以引用 s2 指向的是常量池那块内存。所以第四行 s1 指向堆中内存,s2 指向的是堆中的常量池中的内存。自然而然应该是 false 。图解
上图是我的理解,先说一下,这是错的!错在哪呢?错在了第二行代码,s1.intern() 的时候,并不是把字符串常量 "11" 放入常量池,而是由于此时堆空间已存在字面量为 "11" 的对象,那么调用 intern() 方法的时候 JVM 会把堆空间已存在的那个对象的引用给保存到字符串常量池。正确的应该是下面的图
如果这一步能理解的通,那么后面就简单了,答案也就显而易见了。但是我其实一直很难以接受,但是又找不到其他解释,也只能接受这种说法吧。
仔细分析这几行代码
上面的内容其实还是蛮有深度的,没有扎实底层基础的Java开发应该难理解这几行代码涉及到的功能影响。那么下面结合字节码来深入分析这几行代码。
String s1 = new String("1") + new String("1");
这一行代码可以说是大有学问,直接看字节码
首先实例化一个 StringBuilder 对象用来准备连接字符串,有经验的同学都知道Java中的 "+" 操作符,底层是用 StringBuilder 来连接的。然后 第一个 new String("1") 在堆空间开辟内存,并且把 "1" 字面量放进字符串常量池,这里在 "ldc" 字节码指令体现,这个指令就是将int、float或String型常量值从常量池中推送至栈顶。然后调用 StringBuilder 的 append 方法先拼第一个 new String("1") 。接着再一次 new String("1") 这一步和上面一样又开辟了一块内存空间。然后再调用 StringBuilder 的 append 方法,最后行号 31 调用了 StringBuilder 的 toString 方法,并且把返回值给 s1 。那么我们可以去看看 StringBuilder 的 toString 方法,发现它又 new 了一个 String 返回。所以,s1 最后指向的其实是一个区分于两个 new String("1") 新开辟的一块内存空间。然后并没有 "ldc" 后面 String 11 的操作。上面我们可以看到,只有 "ldc" 后面跟着 String 1,表示把字符串 "1" 放进常量池。所以这里 "11" 并没有放入常量池。
s1.intern();
要弄懂这一行代码,首先要知道 intern() 方法是干啥的,这个简单,直接去看源码就知道了。但是我们去 String 类会发现,这玩意是个 native 方法。
别慌,既然看不到代码怎么写,那就看注释吧。还好注释说的很详细,大致意思呢就是,当调用这个方法的时候,如果常量池已经包含了这个字符串字面量,那么就返回常量池里面这个字符串,如果还没有,那么就把当前字符串放入常量池,然后再返回。这就好办了,由于第一步 s1 表示的字符串 "11" 并没有放入常量池,也就是说此时常量池没有 "11" 。所以这个方法的调用理应把 "11" 放入常量池。如果说堆空间 (除了常量池以外的堆区域) 没有 "11" 的字面量实例,那么的确如此。但是堆空间已经存在了,那么 JVM 会节省空间消耗,直接把堆中代表 "11" 字面量实例的引用给保存到常量池。(在这个例子里,这个 s1.intern() 方法用不用变量接收其实没有影响,但是值得注意的是在很多例子中,这个方法用不用变量接收返回值还是会对下面的代码有很大影响的)。
String s2 = "11";
这段代码会把 "11" 放入常量池,但是此时常量池中已经拥有了 指向 "11" 字面量的引用,所以,s2 指向的其实是常量池中的 s1 引用,因为 s1 引用又指向堆中的 "11" 实例。所以 s1 和 s2 最终的指向同一个地址,所以上面的代码输出结果是 true。
这道题目看似简单,但是背后蕴含大量的底层知识,可以说完全能体现出一个 Java 程序员的底层水平。那么将上面的题目变形一下再看呢?
String s2 = "11"; String s1 = new String("1") + new String("1"); s1.intern(); System.out.println(s1 == s2);
我现在把 s2 的代码给拿到 s1 上面去,这样输出的结果就变成了 false 。如果上面的题目懂了的话,这里也能清楚的理解。由于第一行字符串常量池中没有 "11" ,所以 s2 指向的是常量池中的地址。第二行 s1 指向的是堆中(除了常量池之外的堆区域)的 "11" 实例内存地址。第三行 s1.intern() 方法由于常量池中已经存在 "11" 所以会返回常量池中的字符串,但是!要注意,这里并没有用变量来接收啊!所以这一行代码执行不执行,写不写其实对程序影响不大。如果说用一个临时变量 s3 来接收 s1.intern() 再比较 s3 和 s2 ,那么结果稳稳的是 true。但是这里没有用变量来接收 s1.intern() 所以 s1 指向的仍然是堆空间的那个 "11" 实例。而 s2 指向的字符串常量池的那块内存。所以答案是 false 。