携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第5天,点击查看活动详情
首先我们看看这份代码,是否能够正确地说出打印结果?
public static void main(String[] args) {
String s1 = "a";
String s2 = "b";
String s3 = "ab";
String s4 = "a" + "b";
String s5 = s1 + s2;
String s6 = new String("ab");
System.out.println(s3 == s4);
System.out.println(s3 == s5);
System.out.println(s3 == s6);
System.out.println(s5 == s6);
}
运行结果为:
true
false
false
false
那么它的底层原理是什么呢?
我们使用反编译工具 javap 工具查看一下,javap 工具的使用如下:
javap -v class文件
首先,先了解几个字节码指令的作用:
ldc从常量池中获取常量,并推入到操作数栈中astore将操作数栈中的栈顶元素存放到局部变量表中new创建一个对象实例,并压入操作数栈aload将局部变量表的值推入到操作数栈中invokevirtual调用实例方法dup复制操作数栈栈顶元素并压入栈顶invokespecial调用实例初始化方法(构造方法)、私有方法或父类方法invokevirtual调用对象实例方法
类的字节码文件中会有常量池部分存放常量,在类加载后这些常量会加载到运行时常量池中。
操作数栈是方法执行过程的一种数据结构,对变量的操作总是在操作数栈的栈顶上执行的。
局部变量表负责存放方法体内的局部变量。
我们只查看反编译后的方法主体,它的字节码是这样的,我们先看看全部内容,待会逐个分析。
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=7, args_size=1
0: ldc #2 // String a
2: astore_1
3: ldc #3 // String b
5: astore_2
6: ldc #4 // String ab
8: astore_3
9: ldc #4 // String ab
11: astore 4
13: new #5 // class java/lang/StringBuilder
16: dup
17: invokespecial #6 // Method java/lang/StringBuilder."<init>":()V
20: aload_1
21: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
24: aload_2
25: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
28: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
31: astore 5
33: new #9 // class java/lang/String
36: dup
37: ldc #4 // String ab
39: invokespecial #10 // Method java/lang/String."<init>":(Ljava/lang/String;)V
42: astore 6
44: getstatic #11 // Field java/lang/System.out:Ljava/io/PrintStream;
47: aload_3
48: aload 4
50: if_acmpne 57
53: iconst_1
54: goto 58
57: iconst_0
58: invokevirtual #12 // Method java/io/PrintStream.println:(Z)V
61: getstatic #11 // Field java/lang/System.out:Ljava/io/PrintStream;
64: aload_3
65: aload 5
67: if_acmpne 74
70: iconst_1
71: goto 75
74: iconst_0
75: invokevirtual #12 // Method java/io/PrintStream.println:(Z)V
78: getstatic #11 // Field java/lang/System.out:Ljava/io/PrintStream;
81: aload_3
82: aload 6
84: if_acmpne 91
87: iconst_1
88: goto 92
91: iconst_0
92: invokevirtual #12 // Method java/io/PrintStream.println:(Z)V
95: getstatic #11 // Field java/lang/System.out:Ljava/io/PrintStream;
98: aload 5
100: aload 6
102: if_acmpne 109
105: iconst_1
106: goto 110
109: iconst_0
110: invokevirtual #12 // Method java/io/PrintStream.println:(Z)V
113: return
现在,我们逐个分析。
【1】 String s1 = "a";
【2】 String s2 = "b";
【3】 String s3 = "ab";
【4】 String s4 = "a" + "b";
【5】 String s5 = s1 + s2;
【6】 String s6 = new String("ab");
对于【1】【2】【3】三条语句,JVM 会从常量池中获取实例并赋值给字符串变量。
0: ldc #2 // String a 井号+数字是常量池对应的索引,这里对应字符串 "a"
2: astore_1
3: ldc #3 // String b
5: astore_2
6: ldc #4 // String ab
8: astore_3
对于【4】语句,JVM 会优化为 String s4 = "ab",因此,他实际上和 s3 字符串引用的是同一个常量池对象。
9: ldc #4 // String ab
11: astore 4
对于【5】语句,JVM 会 new 一个 StringBuilder 对象,并使用其方法 append 拼接字符串,最后调用 toString() 方法赋值给变量,字符串实例存在于堆内存中。
# 【创建一个对象实例】
13: new #5 // class java/lang/StringBuilder
# 【复制一份对象实例并压入栈顶】
16: dup
# 【执行初始化,调用构造方法】
17: invokespecial #6 // Method java/lang/StringBuilder."<init>":()V
# 【获取局部变量表 1 号位置的变量,即 "a",压入到操作数栈中】
20: aload_1
# 【调用 append 方法,将 "a" 字符串拼接到 StringBuilder 对象上】
21: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
# 【获取局部变量表 2 号位置的变量,即 "b",压入到操作数栈中】
24: aload_2
# 【调用 append 方法,将 "b" 字符串拼接到 StringBuilder 对象上】
25: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
# 【调用 toString 方法,获得 "ab" 的对象实例,存储到局部变量表上】
28: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
31: astore 5
对于【6】语句,new 关键字会在堆内存中创建一个新的实例,因此和上面的其他变量引用的都不会是同一个对象。
33: new #9 // class java/lang/String
36: dup
37: ldc #4 // String ab
39: invokespecial #10 // Method java/lang/String."<init>":(Ljava/lang/String;)V
42: astore 6
总结
字符串的声明,如果是使用常量声明,那么该字符串实例存在于字符串池中,是一个常量。如果后面有相同的声明方式,那么会引用同一个常量。
如果使用new关键字,那么会在堆内存中创建一个对象实例。
字符串的拼接,我们可以分两种情况:
- 如果是常量拼接,那么 JVM 会优化掉
+操作符,使用字符串池中的对象,如果没有则在字符串池中创建 - 如果是变量拼接,那么 JVM 会创建一个
StringBuilder实例,调用append方法拼接,然后调用toString()获得一个堆内存的String实例。例如String str = s1 + s2;或者String str = "a" + s2;