两个代码例子:
- 例1:
public static void main(String[] args) { String testStr = "test"; String rst = testStr + 1 + "a" + "pig" + 2; System.out.println(rst); } 复制代码
- 例2:
public static void main(String[] args) { String testStr = ""; for (int i = 0; i < 10; i++) { testStr += i + "test"; } System.out.println(testStr); } 复制代码
分析:
- 用javap反编译
例1
中代码的class文件得到如下字节码表示:
可见javac自动将这种连续的字符串拼接编译成了StringBuilder.append,因此写代码时不用自己刻意写成StringBuilder.append形式;public static Method main:"([Ljava/lang/String;)V" stack 3 locals 3 { ldc String "test"; astore_1; new class java/lang/StringBuilder; dup; aload_1; invokestatic Method java/lang/String.valueOf:"(Ljava/lang/Object;)Ljava/lang/String;"; invokespecial Method java/lang/StringBuilder."<init>":"(Ljava/lang/String;)V"; iconst_1; invokevirtual Method java/lang/StringBuilder.append:"(I)Ljava/lang/StringBuilder;"; ldc String "a"; invokevirtual Method java/lang/StringBuilder.append:"(Ljava/lang/String;)Ljava/lang/StringBuilder;"; ldc String "pig"; invokevirtual Method java/lang/StringBuilder.append:"(Ljava/lang/String;)Ljava/lang/StringBuilder;"; iconst_2; invokevirtual Method java/lang/StringBuilder.append:"(I)Ljava/lang/StringBuilder;"; invokevirtual Method java/lang/StringBuilder.toString:"()Ljava/lang/String;"; astore_2; getstatic Field java/lang/System.out:"Ljava/io/PrintStream;"; aload_2; invokevirtual Method java/io/PrintStream.println:"(Ljava/lang/String;)V"; return; } 复制代码
- 用javap反编译
例2
中代码的class文件得到如下字节码表示:
有点长,我稍微加了些注释以便阅读:可见javac仍然自动将循环体内的字符串拼接自动编译成了StringBuilder.append,但是请注意,public static Method main:"([Ljava/lang/String;)V" stack 3 locals 3 { ldc String ""; // 栈上来个空字符串 astore_1; // 栈顶元素弹出,放到局部变量1 iconst_0; // 栈上来个0 istore_2; // 栈顶元素弹出,放到局部变量2 goto L35; // 跳转到35行,此时栈空 L8: stack_frame_type append; locals_map class java/lang/String, int; new class java/lang/StringBuilder; // 栈上来个StringBuilder对象 dup; // 复制栈顶对象,也压入栈中(StringBuilder对象) aload_1; // 局部变量1压入栈中 invokestatic Method java/lang/String.valueOf:"(Ljava/lang/Object;)Ljava/lang/String;"; // 栈顶元素强转String invokespecial Method java/lang/StringBuilder."<init>":"(Ljava/lang/String;)V"; // 弹出栈顶2个元素,调用StringBuilder构造函数,剩下一个刚才复制的、现在已经初始化好的StringBuilder在栈顶 iload_2; // 局部变量2压入栈中:数字0 invokevirtual Method java/lang/StringBuilder.append:"(I)Ljava/lang/StringBuilder;"; // 数字0 append进栈顶StringBuilder,返回值StringBuilder放回栈顶 ldc String "test"; // 常量"test"压入栈中 invokevirtual Method java/lang/StringBuilder.append:"(Ljava/lang/String;)Ljava/lang/StringBuilder;"; // “test”append进栈顶StringBuilder,返回值StringBuilder放回栈顶 invokevirtual Method java/lang/StringBuilder.toString:"()Ljava/lang/String;"; // 栈顶StringBuilder弹出,toString,String结果放入栈顶 astore_1; // String结果弹出放进局部变量1 iinc 2, 1; // 2号局部变量++ L35: stack_frame_type same; iload_2; // 2号局部变量放到栈顶 bipush 10; // 常量10压栈,作为栈顶 if_icmplt L8; // 弹出栈中前两个数,如果栈中第二个数比栈顶元素小,就跳到第8行,否则继续往下 getstatic Field java/lang/System.out:"Ljava/io/PrintStream;"; // PrintStream对象压入栈顶 aload_1; // 局部变量1压入栈顶(字符串) invokevirtual Method java/io/PrintStream.println:"(Ljava/lang/String;)V"; // 弹出栈中2个数,以栈中第二个元素作为参数1(即this),栈顶元素作为参数2,调用println方法 return; } 复制代码
new class java/lang/StringBuilder;
这条指令出现在了L8
标签和L35
后面的if_icmplt L8;
跳转指令之间,也就是说:它在循环体的内部 —— again也就是说 —— 这段代码被优化成了每次循环都会重新new一个StringBuilder做字符串拼接,循环多少次、就会创建多少个StringBuilder对象;虽然说这可能并非什么大不了的开销,但仍然还是程序员手工把StringBuilder写在循环体外面、然后循环体内仅仅使用append更划算。
总结
由于现代的javac编译器能够自动将字符串拼接编译为StringBuilder.append的形式,因此平时直接书写'+'号拼接即可,但若需要通过一个循环来拼接字符串,则最好将StringBuilder显式地在循环体外创建再在循环体内使用,以避免多次重复创建StringBuilder对象