配合反编译代码验证字符串初始化操作.
相信看到这里, 再见到有关的面试题, 你已经无所畏惧了, 因为你已经懂得了背后原理。
在结束之前我们不妨再做一道压轴题
public class Main { public static void main(String[] args) { String s1 = "hello "; String s2 = "world"; String s3 = s1 + s2; String s4 = "hello world"; System.out.println(s3 == s4); } }
这道压轴题是经过精心设计的, 它不但照应上面所讲的字符串常量池知识, 也引出了后面的话题.
如果看这篇文章是你第一次往底层探索字符串的经历, 那我估计你不能立即给出答案. 因为我第一次见这几行代码时也卡壳了。
首先第一行和第二行是常规的字符串对象声明, 我们已经很熟悉了, 它们分别会在堆内存创建字符串对象, 并会在字符串常量池中进行注册。
影响我们做出判断的是第三行代码 Strings3=s1+s2;
, 我们不知道 s1+s2
在创建完新字符串"hello world"后是否会在字符串常量池进行注册。
说白了就是我们不知道这行代码是以双引号""形式声明字符串, 还是用new关键字创建字符串。
这时, 我们应该去读一读这段代码的反编译代码. 如果你没有读过反编译代码, 不妨借此机会入门。
在命令行中输入 javap-c对应.class文件的绝对路径
, 按回车后即可看到反编译文件的代码段。
class Compiled from "Main.java" public class forTest.Main { public forTest.Main(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public static void main(java.lang.String[]); Code: 0: ldc #2 // String hello 2: astore_1 3: ldc #3 // String world 5: astore_2 6: new #4 // class java/lang/StringBuilder 9: dup 10: invokespecial #5 // Method java/lang/StringBuilder."<init>":()V 13: aload_1 14: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 17: aload_2 18: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 21: invokevirtual #7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; 24: astore_3 25: ldc #8 // String hello world 27: astore 4 29: getstatic #9 // Field java/lang/System.out:Ljava/io/PrintStream; 32: aload_3 33: aload 4 35: if_acmpne 42 38: iconst_1 39: goto 43 42: iconst_0 43: invokevirtual #10 // Method java/io/PrintStream.println:(Z)V 46: return }
- 首先调用构造器完成Main类的初始化
0:ldc#2 // String hello
- 从常量池中获取"hello "字符串并推送至栈顶, 此时拿到了"hello "的引用
2:astore_1
- 将栈顶的字符串引用存入第二个本地变量s1, 也就是s1已经指向了"hello "
3:ldc#3 // String world
5:astore_2
- 重复开始的步骤, 此时变量s2指向"word"
6:new#4 // class java/lang/StringBuilder
- 刺激的东西来了: 这时创建了一个StringBuilder, 并把其引用值压到栈顶
9:dup
- 复制栈顶的值, 并继续压入栈定, 也就意味着栈从上到下有两份StringBuilder的引用, 将来要操作两次StringBuilder.
10:invokespecial#5 // Method java/lang/StringBuilder."<init>":()V
- 调用StringBuilder的一些初始化方法, 静态方法或父类方法, 完成初始化.
- 13: aload_1
- 把第二个本地变量也就是s1压入栈顶, 现在栈顶从上往下数两个数据依次是:s1变量和StringBuilder的引用
14:invokevirtual#6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
- 调用StringBuilder的append方法, 栈顶的两个数据在这里调用方法时就用上了.
- 接下来又调用了一次append方法(之前StringBuilder的引用拷贝两份就用途在此)
- 完成后, StringBuilder中已经拼接好了"hello world", 看到这里相信大家已经明白虚拟机是如何拼接字符串的了. 接下来就是关键环节
21:invokevirtual#7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
24:astore_3
- 拼接完字符串后, 虚拟机调用StringBuilder的
toString()
方法获得字符串hello world
, 并存放至s3. - 激动人心的时刻来了, 我们之所以不知道这道题的答案是因为不知道字符串拼接后是以new的形式还是以双引号""的形式创建字符串对象.
- 下面是我们追踪StringBuilder的
toString()
方法源码:
@Override public String toString() { // Create a copy, don't share the array return new String(value, 0, count); }
ok, 这道题解了, s3是通过new关键字获得字符串对象的。
回到题目, 也就是说字符串常量表中没有存储"hello world"的引用, 当s4以引号的形式声明字符串时, 由于在字符串常量池中查不到相应的引用, 所以会在堆内存中新创建一个字符串对象. 所以s3和s4指向的不是同一个字符串对象, 结果为false。