今天在突然想为什么在连接字符串时,StringBuffer或是StringBuilder(1.5开始)的速度要好于采用连接符“+”。有没有什么特殊的情况呢?java6对编译器对“+”操作做了优化处理后,是否也遵循上述情况呢?
在java6之前,JAVA是如何处理String间的“+”操作呢?我们知道String保存的不是真正的值,它只是栈空间中一个指向堆空间具体内容的地址,且String是final类型的类,这就意味着每次操作都会产生新的String。比如:
String str = “abc”;//“abc”保存在堆空间,str1在栈空间,保存了“abc”在堆空间的地址
str += “def”;
上述操作,第一步就不需要多说了,对于第二步操作,首先在堆空间中创建新的空间,并做“abc”+“def”的操作,然后将新的堆空间地址指向str。
可见,“+”操作就是一个不断创建新对象的过程并在堆空间中复制原关联的值,这个过程会极大的延缓程序的执行效率,所以在类似的字符串不断累加的操作的,提倡使用StringBuffer和StringBuilder,同时由于StringBuffer是线程安全的,所以效率会略低于StringBuilder。
但是从JAVA6开始,上述情况发生了变化,编译器在遇到“+”操作时,会进行一定程度的优化。我们通过,编译后代码可以看到,编译器根据参与“+”操作的变量类型,而采取了优化处理。
- 参与类型都为字符串
源代码 | 编译后代码 |
String str1 = "100" + "abc" + "zxc"; | 0: ldc #2 // String 100abczxc |
- 参与类型为字符串和基本类型
源代码 | 编译后代码 |
String str3 = "100" + 1; | 55: ldc #12 // String 1001 |
String str5 = "100" + 'a' | 85: ldc #17 // String 100a |
String str6 = "100" + 100.0; | 89: ldc #18 // String 100100.0 |
String str7 = "100" + true; | 93: ldc #19 // String 100true |
String str9 = true + "100"; | 97: ldc #20 // String true100 |
String str10 = "100" + 1 + 1.0; | 101: ldc #21 // String 10011.0 |
- 参与类型不定
源代码 | 编译后代码 |
String str2 = "100" + str1 + 1; | 3: new #3 // class java/lang/StringBuilder 6: dup 7: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V 10: ldc #5 // String 100 12: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 15: aload_1 16: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 19: iconst_1 20: invokevirtual #7 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder; 23: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; |
String str4 = "100" + Double.valueOf(100); | 59: new #3 // class java/lang/StringBuilder 62: dup 63: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V 66: ldc #5 // String 100 68: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 71: ldc2_w #13 // double 100.0d 74: invokestatic #15 // Method java/lang/Double.valueOf:(D)Ljava/lang/Double; 77: invokevirtual #16 // Method java/lang/StringBuilder.append:(Ljava/lang/Object;)Ljava/lang/StringBuilder; 80: invokevirtual #10 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; |
String str11 = "100" + new String("abc"); | 76: new #3 // class java/lang/StringBuilder 79: dup 80: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V 83: ldc #5 // String 100 85: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 88: new #19 // class java/lang/String 91: dup 92: ldc #20 // String abc 94: invokespecial #21 // Method java/lang/String."<init>":(Ljava/lang/String;)V 97: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 100: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; |
由上可以看到如果参与“+”运算的类型仅为字符串的时候,直接在堆空间内完成操作,不会有额外的操作了;运算类型为字符串和基本类型时,等同于全部为字符串的情况,都是在堆内完成操作;而运算类型增加了其他对象时,编译器就会首先创建一个StringBuilder对象,然后在StringBuilder对象中进行append操作,结束后再将新生成的字符串的堆地址赋值。
所以上述几种情况下,我们没有必要将“+”操作转换为StringBuffer或是StringBuilder操作了,因为编译器会为我们进行转换,转换后效率相差不会太大。但这样程序可以写的更加的简洁、直观、明了。
但是如果涉及到了循环,会是什么情况呢?
源代码 | 编译后代码 |
String str8 = "100"; for (int i = 0; i < 100; i++) { str8 += i; } | 138: ldc #5 // String 100 140: astore 12 142: iconst_0 143: istore 13 145: iload 13 147: bipush 100 149: if_icmpge 180 152: new #3 // class java/lang/StringBuilder 155: dup 156: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V 159: aload 12 161: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 164: iload 13 166: invokevirtual #7 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder; 169: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; 172: astore 12 174: iinc 13, 1 177: goto 145 |
可以看到每次循环结束后,程序都会重新创建一个新的StringBuilder对象,这样同之前多次创建String对象的情况是一样的,时间浪费在了频繁的创建对象上了,所以在循环的情况下,我们还是采用StringBuilder的好。