《提高String和StringBuffer性能的技巧》一文中提到一个遗留的疑问:
虽然使用+操作符连接字符串的时候+操作符在编译以后会优化地被StringBuffer接管,但是,在大数量级的循环中以+操作符连接字符串的时候为什么效率还是非常低呢?现鄙人将问题解答如下。
首先我们看看两段程序:
Test1.java
public class Test1{
public static void main(String args[]){
StringBuffer sb = new StringBuffer();
String sRes = null;
for(int i = 0 ; i < 1024*1024; i++ ){
sb.append( "XXX" );
}
sRes = sb.toString();
}
}
上面这段代码的执行时间在0.1秒钟以内。
Test2.java
public class Test2{
public static void main(String args[]){
//StringBuffer sb = new StringBuffer();
String sRes = null;
for(int i = 0 ; i < 1024*1024; i++ ){
sRes += "XXX" ;
}
}
}
而Test2.java这段代码的执行时间估计会超过10分钟(我等了两三分钟不耐烦就强行咔嚓了。)
Well,我们再看看两段代码分别编译后的伪代码,便可以发现线索。
C:/temp>javap -c Test1
Compiled from "Test1.java"
public class Test1 extends java.lang.Object{
public Test1();
Code:
0: aload_0
1: invokespecial #1; //Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: new #2; //class StringBuffer
3: dup
4: invokespecial #3; //Method java/lang/StringBuffer."<init>":()V
7: astore_1
8: aconst_null
9: astore_2
10: iconst_0
11: istore_3
12: iload_3
13: ldc #4; //int 1048576
15: if_icmpge 31
18: aload_1
19: ldc #5; //String XXX
21: invokevirtual #6; //Method java/lang/StringBuffer.append:(Ljava/lang/String;)Ljava/lang/StringBuffer;
24: pop
25: iinc 3, 1
28: goto 12
31: aload_1
32: invokevirtual #7; //Method java/lang/StringBuffer.toString:()Ljava/lang/String;
35: astore_2
36: return
}
对于Test1,我们主要关心0、12、28这三行伪码,我们可以发现StringBuffer类的创建是在循环开始之前,也就是说,从程序执行的开始到结束,StringBuffer类的实例始终只有一个。
C:/temp>javap -c Test2
Compiled from "Test2.java"
public class Test2 extends java.lang.Object{
public Test2();
Code:
0: aload_0
1: invokespecial #1; //Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: aconst_null
1: astore_1
2: iconst_0
3: istore_2
4: iload_2
5: ldc #2; //int 1048576
7: if_icmpge 36
10: new #3; //class StringBuffer
13: dup
14: invokespecial #4; //Method java/lang/StringBuffer."<init>":()V
17: aload_1
18: invokevirtual #5; //Method java/lang/StringBuffer.append:(Ljava/lang/String;)Ljava/lang/StringBuffer;
21: ldc #6; //String XXX
23: invokevirtual #5; //Method java/lang/StringBuffer.append:(Ljava/lang/String;)Ljava/lang/StringBuffer;
26: invokevirtual #7; //Method java/lang/StringBuffer.toString:()Ljava/lang/String;
29: astore_1
30: iinc 2, 1
33: goto 4
36: return
}
对于Test2,我们主要关心4、10、33这三行伪码,我们可以发现StringBuffer类的创建是在循环体的内部,这很恐怖--从程序执行的开始到结束,StringBuffer类的实例将被不停地创建并被GC,这样将导致大量的CPU和内存开销。对此我特地用Borland的Optimization Suit中的Profile工具观察过这段程序执行过程中的各种类型的实例数量、CPU负载、内存负载、GC动作等变化现象,可以实证上面的解释,大家有兴趣的话也不妨验证一下。
最后,我们可以得出一个结论:需要在循环中连接运行期决定的String实例的时候,请使用StringBuffer类提供的字符串连接功能来代替+操作符。