字符串拼接在日常中编码中大量使用,但是对底层的原理却缺乏理解。
我们还是从一段代码开始:
public static void main(String[] args) throws Exception{
String s1="a";
String s2="b";
String s3="ab";
String s4=s1+s2;
System.out.println(s3==s4);
}
运行输出:
false
我们反编译字节码看看:
javap -v Jvm1_22
选取关键部分:
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: new #5 // class java/lang/StringBuilder
12: dup //复制一份对象引用到栈顶
13: invokespecial #6 // Method java/lang/StringBuilder."<init>":()V 执行构造方法
16: aload_1
17: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
20: aload_2
21: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
24: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
27: astore 4
29: getstatic #9 // Field java/lang/System.out:Ljava/io/PrintStream;
我们解读如下:
前面字符串"a" “b” "ab"的生成没啥问题,我们关注 String s4=s1+s2的执行过程,
从第9-13行,我们看到new 关键字,其实是创建了StringBuilder对象
16行加载了第一个槽位中的数a
17行调用了append的操作
10行加载了第二个槽位中的数,21行调用了append的操作
24行调用了tostring的方法,27行则把对象进行保存。
我们再进一步看StringBuilder中的toString方法:
@Override
public String toString() {
// Create a copy, don't share the array
return new String(value, 0, count);
}
可以很清楚看到,这里会利用拼接好的值new一个新的String对象,所以我们最终拼接的结果会是一个新对象,这就是a+b的生成过程,可以很好解释s3==s4结果是false。
总结
字符串的+ 拼接操作其实底层会调用StringBuilder拼接一个新的字符串对象出来,有new的操作,返回的是一个新的对象。
进一步理解
我们看看另一种情况:
public static void main(String[] args) throws Exception{
String s1="a";//用到了这个对象,才开始创建这个对象,行为上面是懒惰的
String s2="b";
String s3="ab";
String s4=s1+s2;
System.out.println(s3==s4);
String s5="a"+"b";
System.out.println(s5==s3);
}
反编译结果如下:
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: ldc #4 // String ab
48: astore 5
50: getstatic #9 // Field java/lang/System.out:Ljava/io/PrintStream;
53: aload 5
我们看到46行,字符串直接就取了ab的值,自然输出结果就是true。
这里的解释是javac在编译期的优化,它认为"a" “b"都是常量,拼接的结果在编译期间就确定为"ab”,不可能再变回了,上一行代码是引用的值,有可能发现修改。