Java中有个经典的问题,如下代码的输出结果:
String s1 = "a";
String s2 = s1 + "b";
String s3 = "a" + "b";
System.out.println(s2 == "ab");
System.out.println(s3 == "ab");
答案可以在这里直接给出,输出结果为 false和true,而给出的解释是:对于字符串的直接拼接,会在编译阶段编译器将拼接结果计算出来,并将结果放在常量区中。
对于这个题目来说,s1是变量,导致s2需要在运行时才能拼接出来;而s3是a和b两个常量拼接的结果,所以在编译时就已经得出结果。这就导致了,s2指向的是运行时生成的ab,而s3指向的是编译时存放在常量区的ab。
上面的这种解释很笼统抽象,为了更深入地理解原理,需要查看上面代码编译后的字节码:
stack=3, locals=4, args_size=1
0: ldc #16 // String a
2: astore_1
3: new #18 // class java/lang/StringBuilder
6: dup
7: aload_1
8: invokestatic #20 // Method java/lang/String.valueOf:(Ljava/lang/Object;)Ljava/lang/String;
11: invokespecial #26 // Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V
14: ldc #29 // String b
16: invokevirtual #31 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
19: invokevirtual #35 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
22: astore_2
23: ldc #39 // String ab
25: astore_3
// 下面的代码是打印逻辑
......
在上面的字节码中,重点看3-22行,等价于:
String s2=new StringBuilder(s1).append("b").toString();
而23-25行,说明s3是直接通过符号引用指向常量区中的String类型的ab。
由此我们得出结论:Java中带有变量的字符串拼接,实际上是通过StringBuilder的append方法完成的;而若干个常量的拼接,会在编译阶段编译器"聪明"地完成,运行时直接引用常量区的字段即可。很显然,两种方式对应着两个不同的引用。
所以,考虑到编译阶段的优化,字符串的拼接就可以无所忌惮地用+了吗?答案是否定的!考虑下面两段代码:
代码1:
String a = "test";
for (int i = 0; i < 10; i++) {
a += i;
}
代码2:
StringBuilder builder = new StringBuilder("test");
for (int i = 0; i < 10; i++) {
builder.append(i);
}
String a = builder.toString();
上面的两端代码从完成的功能角度看都是一致的:最终变量a的值都会变成test0123456789。但是查看字节码后:
代码1字节码:
stack=3, locals=2, args_size=0
0: ldc #70 // String test
2: astore_0
3: iconst_0
4: istore_1
5: goto 30
8: new #18 // class java/lang/StringBuilder
11: dup
12: aload_0
13: invokestatic #20 // Method java/lang/String.valueOf:(Ljava/lang/Object;)Ljava/lang/String;
16: invokespecial #26 // Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V
19: iload_1
20: invokevirtual #72 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
23: invokevirtual #35 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
26: astore_0
27: iinc 1, 1
30: iload_1
31: bipush 10
33: if_icmplt 8
36: return
代码2字节码:
stack=3, locals=2, args_size=0
0: new #18 // class java/lang/StringBuilder
3: dup
4: ldc #70 // String test
6: invokespecial #26 // Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V
9: astore_0
10: iconst_0
11: istore_1
12: goto 24
15: aload_0
16: iload_1
17: invokevirtual #72 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
20: pop
21: iinc 1, 1
24: iload_1
25: bipush 10
27: if_icmplt 15
30: aload_0
31: invokevirtual #35 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
34: astore_1
35: return
在代码1的字节码中,8-27行发现,循环体中每一次循环都会new出一个StringBuilder,对应的java代码为:
String a = "test";
for (int i = 0; i < 10; i++) {
a = new StringBuilder(a).append(i).toString();
}
在代码2的字节码中,15-27的循环体中,并不会每次一次循环new出一个StringBuilder。
显然,代码2的执行效率要高于代码1的执行效率。究其缘由是,编译器虽然会在编译阶段对字符串的拼接做了优化,但是并没有做到跨越代码块级别的优化。所以在每一次循环的代码块中,代码1都会new出一个StringBuilder。如果循环次数更多,会创建出更多的StringBuilder对象,最终有可能导致OOM等问题。