面試的時候可能常常會問這樣一個問題:請說一下String、StringBuilder、StringBuffer的區別,可能不少人會說String若是經過+
(這是加好,後面同理)來拼接字符串時,會建立不少臨時變量,性能比較低(網上不少帖子也是這麼寫的),可是,真的是這樣的嗎?java
那麼String經過+
來拼接字符串時,到底有沒有建立臨時變量呢?其實,這個問題很簡單,只須要經過javap
反編譯生成的class文件,看看class文件中String所作的操做就能夠了。下面咱們就以《java編程思想》中字符串章節的例子來說解。面試
首先咱們來看下面這段代碼:編程
public class Test {
public static void main(String[] args){
String mango = "mango";
String s = "abc" + mango + "def" + 47;
System.out.println(s);
}
}
複製代碼
這段代碼是比較典型的經過+
來拼接字符串的代碼,接下來咱們經過javac Test.java
來編譯這段代碼,而後經過javap -c Test.class
反編譯生成的Test.class
文件。剔除掉一些無關的部分,主要展現了main()
中代碼的字節碼,因而有了如下的字節碼。你會發現很是有意思的東西發生了。segmentfault
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: ldc #2 // String mango
2: astore_1
3: new #3 // class java/lang/StringBuilder
6: dup
7: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V
10: ldc #5 // String abc
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: ldc #7 // String def
21: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
24: bipush 47
26: invokevirtual #8 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
29: invokevirtual #9 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
32: astore_2
33: getstatic #10 // Field java/lang/System.out:Ljava/io/PrintStream;
36: aload_2
37: invokevirtual #11 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
40: return
}
複製代碼
這裏涉及到彙編語言,讀者能夠網上搜索字節碼指令表會有不少,我這裏提供一個,讀者能夠對着表來理解每個指令的含義,這裏我不詳細展開。每一個指令的後面可能會有//
,//
後面的內容表示指令碼操做的對象。細心的讀者必定會發現:編譯器自動引入了java.lang.StringBuilder(其中java前面的L
表示引用類型,想要詳細瞭解的讀者能夠看一下《深刻理解Java虛擬機》中類文件結構那一章)。雖然咱們在源代碼中沒有使用StringBuilder,可是編譯器卻自做主張的使用了它,由於它更高效。數組
看上面的字節碼你會發現,編譯器建立StringBuilder對象以後,對+
號相連的每個字符串使用append()
方法來拼接,總計調用了四次,最後調用toString()方法生成結果。(注:讀者感興趣的話能夠用StringBuilder來替換上面的代碼,經過javac
javap
從新編譯,而後你會發現main()
方法中生成的字節碼是同樣的)。bash
經過上面的例子咱們發現,當咱們經過+
來拼接字符串時,編譯器會自動替咱們優化成StringBuilder來拼接,並不會形成網上所說的建立臨時變量,速度變慢這些缺點。(注:因爲StringBuidler是在jdk5.0以後引入的,因此jdk5.0以前是經過StringBuffer來拼接的,感興趣的讀者能夠自行驗證)。app
如今,咱們確定會很開心,既然編譯器都替咱們優化了,那咱們是否是能夠隨意使用String
了呢(想一想都開心)。哈哈,不要高興得太早,由於有時候編譯器的優化可能並非你想要的結果。讓咱們來看下面這段代碼:jvm
下面這段程序採用兩種方式生成一個String:方法一使用多個String對象;方法二代碼中使用了StringBuidler。性能
public class Test {
public String testString(String[] fields) {
String result = "";
for (int i = 0; i < fields.length; i++) {
result += fields[i];
}
return result;
}
public String testStringBuilder(String[] fields){
StringBuilder result = new StringBuilder();
for (int i = 0; i<fields.length; i++){
result.append(fields[i]);
}
return result.toString();
}
}
複製代碼
上面代碼中的兩個方法執行相似,都是傳入字符串數組,而後經過for
循環將數組字符串拼接起來,區別是第一個方法使用String
來拼接,第二個方法使用StringBuilder
來拼接。而後咱們仍是經過javap
來反編譯這段代碼,剔除無關部分,會看到兩個方法的字節碼。優化
首先是testString()
方法的字節碼:
public java.lang.String testString(java.lang.String[]);
descriptor: ([Ljava/lang/String;)Ljava/lang/String;
flags: ACC_PUBLIC
Code:
stack=3, locals=4, args_size=2
0: ldc #2 // String
2: astore_2
3: iconst_0
4: istore_3
5: iload_3
6: aload_1
7: arraylength
8: if_icmpge 38
11: new #3 // class java/lang/StringBuilder
14: dup
15: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V
18: aload_2
19: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
22: aload_1
23: iload_3
24: aaload
25: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
28: invokevirtual #6 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
31: astore_2
32: iinc 3, 1
35: goto 5
38: aload_2
39: areturn
複製代碼
這裏讀者看一下第8行中的if_icmpge
,對照字節碼指令表會發現這個指令就是for循環中比較i
值等於某個值時進入循環,後面的38表示在38行跳出循環,這裏的循環體是第8
行到第35
行。第35行的意思是:返回循環體的起始點(第5
行)。而後咱們看循環體(第8
行到第35
行)中第11行,是一個new
指令,這個太熟悉了,就是建立對象。可是它竟然是在循環體內部,這就意味着每循環一次,就要建立一個新的StringBuilder
對象。這顯然不能接受。
那咱們再看一下testStringBuilder()
的字節碼:
public java.lang.String testStringBuilder(java.lang.String[]);
descriptor: ([Ljava/lang/String;)Ljava/lang/String;
flags: ACC_PUBLIC
Code:
stack=3, locals=4, args_size=2
0: new #3 // class java/lang/StringBuilder
3: dup
4: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V
7: astore_2
8: iconst_0
9: istore_3
10: iload_3
11: aload_1
12: arraylength
13: if_icmpge 30
16: aload_2
17: aload_1
18: iload_3
19: aaload
20: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
23: pop
24: iinc 3, 1
27: goto 10
30: aload_2
31: invokevirtual #6 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
34: areturn
複製代碼
咱們能夠看到,不只循環體部分的代碼更簡短、更簡單了,並且new
只在剛開始調用了一次,說明只生成了一個StringBuilder對象。
因此,當你爲一個類編寫toString()
方法時,若是字符串操做比較簡單,那就能夠信賴編譯器,它會爲你合理的構造最終的字符串結果。可是,若是你要在toString()
方法中使用循環,那麼你就須要本身建立一個StringBuilder對象。固然,當你拿不定主意的時候,那麼你隨時能夠經過javap來分析你的程序。
留一個問題:枚舉你們應該都知道,可是你知道它在jvm中究竟是怎麼執行的嗎?(思路:其實要想知道它的原理很簡單,你一樣能夠寫一段枚舉代碼,而後經過javap
反編譯這段代碼,你會有一種豁然開朗的感受。)
-《java編程思想》