Java中有个经典的问题,如下代码的输出结果:

String s1 = "a";
	String s2 = s1 + "b";
	String s3 = "a" + "b";
	System.out.println(s2 == "ab");
	System.out.println(s3 == "ab");

答案可以在这里直接给出,输出结果为 falsetrue,而给出的解释是:对于字符串的直接拼接,会在编译阶段编译器将拼接结果计算出来,并将结果放在常量区中。

对于这个题目来说,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等问题。