資深 Java 開發者都知道迴圈中用 StringBuffer(或 StringBuilder)替代 String 串接能大幅提升效能,但少有人親眼見過字節碼層面的差異。JDK 內建 javap 工具能將類文件反組譯為 JVM 指令集,揭示編譯器背後的真實行為。
javap -c ClassName # 反組譯方法字節碼
javap -v ClassName # 詳細資訊(常量池、行號表、局部變數表等)
javap -p ClassName # 包含 private 成員
javap -s ClassName # 顯示方法簽名
public class JavapTip {
private static String withStrings(int count) {
String s = "";
for (int i = 0; i < count; i++) {
s += i; // 每次迴圈隱式建立 StringBuilder
}
return s;
}
private static String withStringBuffer(int count) {
StringBuffer sb = new StringBuffer();
for (int i = 0; i < count; i++) {
sb.append(i); // 直接追加,無新物件
}
return sb.toString();
}
}
Method java.lang.String withStrings(int)
0 ldc #2 // ""
2 astore_1 // s = ""
3 iconst_0
4 istore_2 // i = 0
5 goto 30
8 new #3 // new StringBuilder() ← 關鍵:每次迴圈 NEW
11 dup
12 invokespecial #4 // StringBuilder.()
15 aload_1 // 載入 s
16 invokevirtual #5 // StringBuilder.append(String)
19 iload_2 // 載入 i
20 invokevirtual #6 // StringBuilder.append(int)
23 invokevirtual #7 // StringBuilder.toString()
26 astore_1 // s = 新字串
27 iinc 2 1 // i++
30 iload_2
31 iload_0
32 if_icmplt 8 // 迴圈判斷
35 aload_1
36 areturn
Method java.lang.String withStringBuffer(int)
0 new #3 // new StringBuilder() ← 僅一次
3 dup
4 invokespecial #4
7 astore_1 // sb = 新 StringBuilder
8 iconst_0
9 istore_2 // i = 0
10 goto 22
13 aload_1 // 載入 sb
14 iload_2 // 載入 i
15 invokevirtual #6 // sb.append(int)
18 pop // 捨棄返回值
19 iinc 2 1 // i++
22 iload_2
23 iload_0
24 if_icmplt 13 // 迴圈判斷
27 aload_1
28 invokevirtual #7 // sb.toString()
31 areturn
| 指標 | String += | StringBuffer.append |
|---|---|---|
迴圈內 new 指令 | 每次迴圈執行一次(new #3) | 零次 |
| 物件建立數量 | O(N) 個 StringBuilder + O(N) 個 String | 1 個 StringBuffer + 1 個 String |
| 記憶體分配壓力 | 極大,頻繁觸發 Minor GC | 極小 |
| 字節碼長度 | 較長(含 toString/append 多次調用) | 精簡 |
現代 javac (JDK 9+) 對 String += 有逃逸分析與標量替換優化,若變數不逃逸出方法,可能直接在棧上分配 StringBuilder 並消除 toString() 呼叫。但不建議依賴編譯器優化,理由:
StringBuilder 語義清晰、效能可預期、程式碼可讀性更高。final、內聯、逃逸分析)。invokedynamic、Lambda、Method Handle 實現機制。javap -c 是洞察 Java 程式真實運行機制的「X 光機」。通過字節碼對比,我們親眼證實了 String += 在迴圈中隱式建立大量臨時物件,而 StringBuffer/StringBuilder 以單一可變緩衝區實現 O(1) 擴容追加。這不僅是效能優化的鐵律,更是理解 JVM 執行模型的最佳切入點。