017:为什么不建议在循环中使用“+”拼接字符串

典型答案

由于字符串对象是不可变的,所以每次循环都会对操作符左右两边的字符串进行拷贝,并生成一个新的字符串对象。如果循环n次,则这个过程需要n的平方级的时间;并且在这个过程中还创建了很多短命的中间对象。

如果要使用循环构建一个大的字符串,推荐使用StringBuilder代替String,使用StringBuilder的append()方法进行字符串连接,并在循环结束后将StringBuilder对象转为String对象。StringBuilder的原理是预先分配了一个足够大小的缓冲区,然后循环的过程就是往缓冲区里填充数据,比使用“+”做字符串连接的效率要高很多。

知识点梳理

上面的答案是理论知识,这里看下实际案例,假设有如下代码,循环10000次将随机长度80的字符串连接为一个大的字符串,使用“+”和使用StringBuilder的方法之间的差距是两个数量级(我的环境:Mac OS 16G + JDK 1.8)

public class StringConcatExample {

    public static void main(String[] args) {
        long s1 = System.currentTimeMillis();
        new StringConcatExample().implicit();
        System.out.println("使用\"+\"拼接:" + (System.currentTimeMillis() - s1));

        s1 = System.currentTimeMillis();
        new StringConcatExample().exlicit();
        System.out.println("使用\"StringBuilder\"拼接:" + (System.currentTimeMillis() - s1));
    }

    public String implicit() {
        String result = "";
        for (int i = 0; i < 10000; i++) {
            result += (i + ",");
        }
        return result;
    }

    public String exlicit() {
        StringBuilder result = new StringBuilder();
        for (int i = 0; i < 10000; i++) {
            result.append(i).append(",");
        }
        return result.toString();
    }
}

上述代码的运行结果如下:

使用javac StringConcatExample.java命令编译源文件,使用javap -c StringConcatExample命令查看字节码文件的内容。

先看implicit()方法的字节码:

  public java.lang.String implicit();
    Code:
       0: ldc           #16                 // String
       2: astore_1
       3: iconst_0
       4: istore_2
       5: iload_2
       6: sipush        10000
       9: if_icmpge     42
      12: new           #7                  // class java/lang/StringBuilder
      15: dup
      16: invokespecial #8                  // Method java/lang/StringBuilder."<init>":()V
      19: aload_1
      20: invokevirtual #10                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      23: iload_2
      24: invokevirtual #17                 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
      27: ldc           #18                 // String ,
      29: invokevirtual #10                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      32: invokevirtual #12                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
      35: astore_1
      36: iinc          2, 1
      39: goto          5
      42: aload_1
      43: areturn

可以看出,第9行到第39行构成了一个循环体:在第9行的时候做条件判断,如果不满足循环条件,则跳转到42行。编译器做了一定程度的优化,在12行new了一个StringBuilder对象,然后再20行、24行、29进行了三次append方法的调用,不过重点是,每次循环都会new一个StringBuilder对象。

再看explicit()方法的字节码,如下所示:

 public java.lang.String exlicit();
    Code:
       0: new           #7                  // class java/lang/StringBuilder
       3: dup
       4: invokespecial #8                  // Method java/lang/StringBuilder."<init>":()V
       7: astore_1
       8: iconst_0
       9: istore_2
      10: iload_2
      11: sipush        10000
      14: if_icmpge     34
      17: aload_1
      18: iload_2
      19: invokevirtual #17                 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
      22: ldc           #18                 // String ,
      24: invokevirtual #10                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      27: pop
      28: iinc          2, 1
      31: goto          10
      34: aload_1
      35: invokevirtual #12                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
      38: areturn

14行到31行构成了循环体,可以看出,在第4行(循环体外)就构建好了StringBuilder对象,然后再循环体内只进行append()方法的调用。这就从字节码层面解释了为什么不建议在循环体内使用“+”执行字符串的拼接。

参考资料

  1. 《Effective Java(第二版)》
  2. 《Java编程思想》
017:为什么不建议在循环中使用“+”拼接字符串
滚动到顶部