典型答案
由于字符串对象是不可变的,所以每次循环都会对操作符左右两边的字符串进行拷贝,并生成一个新的字符串对象。如果循环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()方法的调用。这就从字节码层面解释了为什么不建议在循环体内使用“+”执行字符串的拼接。
参考资料
- 《Effective Java(第二版)》
- 《Java编程思想》