Java 中 String + String 與 String + char 是開發者每天都在用的操作,但兩者在**字節碼層面、內存分配、性能特徵**上有本質區別。本文從編譯器優化、JVM 實現、字符編碼、性能基準四個維度深度剖析,助你寫出高性能、無 Bug 的拼接代碼。
// 源碼
String s = "Hello" + " " + "World" + userName;
// 編譯器優化(常量折疊 + StringBuilder)
// 1. 編譯期常量折疊:"Hello" + " " + "World" → "Hello World"
// 2. 運行期 StringBuilder 拼接
StringBuilder sb = new StringBuilder("Hello World");
sb.append(userName);
String s = sb.toString();
關鍵點:
StringBuilder.append() 鏈式調用。// 源碼
String s = "Prefix" + 'C' + "Suffix";
// 編譯器翻譯:char 先轉 String,再走 StringBuilder
StringBuilder sb = new StringBuilder("Prefix");
sb.append(String.valueOf('C')); // 關鍵:char → String 轉換
sb.append("Suffix");
String s = sb.toString();
本質區別:char 不是 CharSequence,StringBuilder 無 append(char) 重載直接生成字符串,**必須先轉為 String**(調用 String.valueOf(char) 或 Character.toString(char)),產生額外 String 對象。
// String + String
0: new #2 // java/lang/StringBuilder
3: dup
4: ldc #3 // "Prefix"
6: invokespecial #4 // StringBuilder."":(Ljava/lang/String;)V
9: ldc #5 // "Suffix"
11: invokevirtual #6 // StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
14: invokevirtual #7 // StringBuilder.toString:()Ljava/lang/String;
// String + char
0: new #2 // java/lang/StringBuilder
3: dup
4: ldc #3 // "Prefix"
6: invokespecial #4 // StringBuilder."":(Ljava/lang/String;)V
9: bipush 67 // 'C' = 67
11: invokestatic #8 // String.valueOf:(C)Ljava/lang/String;
14: invokevirtual #6 // StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
17: ldc #5 // "Suffix"
19: invokevirtual #6 // StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
22: invokevirtual #7 // StringBuilder.toString:()Ljava/lang/String;
差異:String + char 多了一條 invokestatic String.valueOf(C),並在堆上分配一個臨時 String 對象(單字符字符串)。
char 是 16 位無符號整數,代表 **UTF-16 代碼單元**。char。char**(代理對 Surrogate Pair)。String emoji = "😀"; // U+1F600
char c = emoji.charAt(0); // 高代理 \uD83D
char c2 = emoji.charAt(1); // 低代理 \uDE00
String s = "Prefix" + c; // 結果:"Prefix\uD83D" —— 破損的字符!
結論:絕不可將單個 char 視為完整字符進行拼接,補充平面字符會被截斷。正確做法用 codePointAt、substring 或直接操作 String。
| 場景 | 操作 | 吞吐量 (ops/ms) | 分配率 (MB/sec) |
|---|---|---|---|
| 單次拼接 | "A" + "B" | ~12,000 | 極低(常量折疊) |
| 單次拼接 | "A" + 'B' | ~10,500 | 低(+1 String 對象) |
| 循環 1000 次 | s += "x" (循環內 new StringBuilder) | ~45 | 高(大量臨時對象) |
| 循環 1000 次 | sb.append("x") (循環外 StringBuilder) | ~3,200 | 極低 |
| 循環 1000 次 | sb.append('x') (循環外 StringBuilder) | ~3,150 | 極低(直接 append(char) 無轉換) |
關鍵發現:
StringBuilder.append(char) **直接接受 char,無轉換開銷**,性能優於 append(String.valueOf(char))。StringBuilder,否則性能差 2 個數量級。StringBuilder.append(char)。StringBuilder sb = new StringBuilder(); sb.append(c); sb.append(str); 顯式調用 append(char)。"Prefix" + ch + "Suffix" 可讀性優先,性能損耗可忽略。char 拼接,改用 String 子串或 codePoint API。String.format、MessageFormat 或 SLF4J 佔位符 "{} {}",避免不必要的字符串構造(日誌級別未啟用時)。StringBuilder 內部同樣優化,append(char) 自動處理擴展。String + String 編譯器優化為 StringBuilder.append(String);String + char 強制經過 String.valueOf(char) 產生臨時字符串對象。**性能差異在單次操作可忽略,但在熱點循環中顯著;更大的風險在於 char 無法正確表示補充平面字符,導致數據損壞**。原則:高頻用 StringBuilder 顯式 append(char);涉及用戶可見文本堅持用 String/代碼點 API;永不假設 char = 字符。