Java:为什么循环体内的字符串拼接值得更改为StringBuilder.append?

两个代码例子:

  1. 例1:
    public static void main(String[] args) {
    	String testStr = "test";
    	String rst = testStr + 1 + "a" + "pig" + 2;
    	System.out.println(rst);
    }
    复制代码
  2. 例2:
    public static void main(String[] args) {
    	String testStr = "";
    	for (int i = 0; i < 10; i++) {
    		testStr += i + "test";
    	}
    	System.out.println(testStr);
    }
    复制代码

分析:

  1. 用javap反编译例1中代码的class文件得到如下字节码表示:
    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;
    	
    }
    复制代码
    可见javac自动将这种连续的字符串拼接编译成了StringBuilder.append,因此写代码时不用自己刻意写成StringBuilder.append形式;
  2. 用javap反编译例2中代码的class文件得到如下字节码表示:
    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;
    	
    }
    复制代码
    有点长,我稍微加了些注释以便阅读:可见javac仍然自动将循环体内的字符串拼接自动编译成了StringBuilder.append,但是请注意,new class java/lang/StringBuilder; 这条指令出现在了L8标签和L35后面的if_icmplt L8;跳转指令之间,也就是说:它在循环体的内部 —— again也就是说 —— 这段代码被优化成了每次循环都会重新new一个StringBuilder做字符串拼接,循环多少次、就会创建多少个StringBuilder对象;虽然说这可能并非什么大不了的开销,但仍然还是程序员手工把StringBuilder写在循环体外面、然后循环体内仅仅使用append更划算。

总结

由于现代的javac编译器能够自动将字符串拼接编译为StringBuilder.append的形式,因此平时直接书写'+'号拼接即可,但若需要通过一个循环来拼接字符串,则最好将StringBuilder显式地在循环体外创建再在循环体内使用,以避免多次重复创建StringBuilder对象

猜你喜欢

转载自juejin.im/post/5d3b265ff265da1b971abbad