「StringBuilder比String快」是Java面試必考題,但「快在哪裡、快多少、什麼場景下快」往往答不透徹。本文通過基準測試數據、源碼深度解析、JVM內存模型三個維度,徹底講透這個經典問題。
// 基準測試:拼接 100,000 次 "a"
String s = "";
for (int i = 0; i < 100000; i++) s += "a"; // String +=
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 100000; i++) sb.append("a"); // StringBuilder.append
// 結果(典型值):
// String: ~45,000 ms (45秒)
// StringBuilder: ~15 ms
// 差距: 3000倍 (3個數量級)
質疑:「append內部調用System.arraycopy,不也是創建數組嗎?」
關鍵區別:
| 維度 | String += | StringBuilder.append |
|---|---|---|
| 對象創建 | 每次創建新String對象 + 新char[] | 僅在擴容時創建新char[],無新String對象 |
| 數據複製 | 每次複製整個舊字符串 + 新內容 | 僅複製新增內容到現有數組尾部 |
| 內存分配 | 頻繁小對象分配 → 年輕代GC壓力大 | 幾何級擴容(2倍) → 分配次數極少 |
| 引用更新 | 變量引用指向新對象,舊對象變垃圾 | 同一對象引用,內部數組就地擴展 |
// Java源碼
s += "a";
// 編譯器展開 (javap -c)
StringBuilder temp = new StringBuilder();
temp.append(s);
temp.append("a");
s = temp.toString(); // 每次循環創建新StringBuilder + 新String + 新char[]
// AbstractStringBuilder.append(String str)
public AbstractStringBuilder append(String str) {
if (str == null) return appendNull();
int len = str.length();
ensureCapacityInternal(count + len); // 關鍵:容量檢查與擴容
str.getChars(0, len, value, count); // 關鍵:直接複製到內部數組
count += len;
return this;
}
// 擴容策略:新容量 = 舊容量*2 + 2
private void ensureCapacityInternal(int minimumCapacity) {
if (minimumCapacity - value.length > 0) {
value = Arrays.copyOf(value, newCapacity(minimumCapacity));
}
}
private int newCapacity(int minCapacity) {
int newCapacity = (value.length << 1) + 2; // 2倍 + 2
return (newCapacity < minCapacity) ? minCapacity : newCapacity;
}
循環第1次: "a" → 堆: String("a") + char[1]
循環第2次: "a" + "a" → 堆: String("aa") + char[2] + 垃圾: String("a") + char[1]
循環第3次: "aa" + "a" → 堆: String("aaa") + char[3] + 垃圾: String("aa") + char[2]
...
循環第N次: 產生 N 個String對象 + N 個char[]數組
年輕代GC頻繁觸發 → Stop-the-world → 性能崩塌
初始: char[16] (默認容量)
追加: 直接寫入 value[count++] = 'a'
擴容點: 16→34→70→142→286→574→1150→2302→4606→9214→18430→36862→73726
總共: 13次擴容, 13次Arrays.copyOf, 0個String對象創建
最終: 1個StringBuilder對象 + 1個char[73726] + 1次toString()創建最終String
synchronized修飾所有公共方法,線程安全,性能略低於StringBuilder需要拼接字符串?
├─ 編譯期常量拼接 ( "a" + "b" + "c" ) → 編譯器優化為單一常量,直接用 +
├─ 循環/大量/未知次數拼接 → StringBuilder (單線程) / StringBuffer (多線程)
├─ 少量/固定次數拼接 ( < 10 次 ) → String += 可接受,現代JVM優化較好
├─ 需要頻繁修改/插入/刪除 → StringBuilder (支持 insert/delete/reverse)
└─ 日誌/調試輸出 → 條件判斷前避免構建: if(log.isDebugEnabled()) log.debug(sb.toString())
new StringBuilder(estimatedSize)避免擴容開銷sb.append(a).append(b).append(c)減少方法調用開銷Appendable接口或StringBuilder本身CharArrayWriter或ByteArrayOutputStream+編碼器StringBuilder之所以快,核心在於可變字符數組+幾何級擴容策略,將O(N²)的重複複製優化為O(N)的單次寫入+少量擴容複製,同時消除了大量臨時對象分配與GC壓力。這是「空間換時間」、「批量處理」、「減少GC」三大性能優化原則的教科書級應用。