許多 Java 開發者都聽過「不要用 + 號拼接字串,要用 StringBuilder」的建議。但這真的是鐵律嗎?本文將從 JVM 位元碼、記憶體模型、編譯器優化三個維度,深度剖析 String、StringBuffer、StringBuilder 的真實效能表現。
根據 Java API 文件明確指出:字串串連是透過 StringBuilder(或 StringBuffer)類及其 append 方法實現的。這意味著 str = a + b + c 在編譯期就會被轉換為 StringBuilder 鏈式呼叫。
// 原始碼
String str = a + b + " !";
// 編譯後等效位元碼(反編譯結果)
String str = new StringBuilder(String.valueOf(a))
.append(b)
.append(" !")
.toString();
當所有運算元皆為字面常量時,javac 會在編譯期直接合併:
// 原始碼
String s = "Hello" + " " + "World";
// 編譯後直接變為
String s = "Hello World";
甚至混合變數與常量時,尾部常量也會被合併:
String java = ", Java!";
String s = "Hello" + " World" + java;
// 編譯後:new StringBuilder("Hello World").append(java).toString()
String result = str1 + str2 + str3 + str4;
// 僅建立一個 StringBuilder,效能最佳
// 反面教材:每次 += 都會 new 一個 StringBuilder
String result = "";
for (String s : list) {
result += s; // 每次迴圈產生新 StringBuilder + 新 String
}
// 正確寫法:單一 StringBuilder 重複使用
StringBuilder sb = new StringBuilder();
for (String s : list) {
sb.append(s);
}
String result = sb.toString();
位元碼對比清楚顯示:迴圈版本在 if_icmpge / goto 迴圈內部重複執行 new StringBuilder,造成大量物件分配與 GC 壓力。
String s1 = "abc"; String s2 = "abc"; 指向同一記憶體位址+ 和 += 是 Java 唯二支援運算子多載的語法糖synchronized,多執行緒安全// 預知最終約 100 字元
StringBuilder sb = new StringBuilder(100); // 避免預設 16 → 擴容複製
for (int i = 0; i < 100; i++) sb.append('a');
sb.append('a'); // 直接操作 char[],零物件分配
sb.append("a"); // 需建立 String 物件,再轉 char[]
若後續仍需繼續拼接,保持 StringBuilder 型態,僅在最終輸出時呼叫 toString()。
| 場景 | 建議做法 | 理由 |
|---|---|---|
| 單行多字串拼接 | 直接用 + 號 | 編譯器自動優化為單一 StringBuilder |
| 迴圈/多行累加 | 顯式 new StringBuilder() | 避免重複建立中間物件 |
| 多執行緒共享緩衝區 | StringBuffer | 內建同步機制 |
| 單執行緒高頻拼接 | StringBuilder + 預設容量 | 零鎖開銷、減少陣列複製 |
| 常量表達式 | 直接寫常量 | 編譯期常量摺疊,零執行期成本 |
理解編譯器如何將 + 號轉換為 StringBuilder,以及何時會產生多餘物件,才是寫出高效能 Java 字串處理程式的關鍵。盲目避免 + 號反而降低程式碼可讀性;善用工具(javap、JMH)驗證效能,才是工程師的正確態度。