String 是 Java 使用頻率最高的類,面試必考、業務必用。本文從源碼層面剖析 String 不變性本質,對比 StringBuilder/StringBuffer 異同,通過字節碼反編譯與基準測試揭示拼接性能真相,並破解流傳已久的「創建對象數量」面試陷阱。
public final class String implements Serializable, Comparable<String>, CharSequence {
private final char value[]; // JDK 8 及之前
// private final byte[] value; // JDK 9+ Compact Strings
private final int hash; // 緩存 hashCode
// ...
public String substring(int begin, int end) {
return (begin == 0 && end == count) ? this : new String(offset + begin, end - begin, value);
}
public String concat(String str) {
char[] buf = new char[count + str.length()];
getChars(0, count, buf, 0);
str.getChars(0, str.length(), buf, count);
return new String(0, count + str.length(), buf);
}
}
String s1 = "hello"; // 編譯期進入 .class 常量池 → 運行期駐留運行時常量池
String s2 = "hello"; // 直接引用常量池同一對象
String s3 = new String("hello"); // 堆中新建對象,內容複製自常量池 "hello"
String s4 = new String("hello"); // 堆中再新建一個對象
s1 == s2 // true (同一常量池引用)
s1 == s3 // false (堆 vs 常量池)
s3 == s4 // false (兩次 new,兩個堆對象)
s3.intern() == s1 // true (intern() 將堆對象駐留/查找常量池)
final String a = "hello"; String b = a + "world"; // 編譯期直接優化為 "helloworld" 常量 // 等價於 String b = "helloworld";
final + 編譯期常量值 → 編譯期常量折疊,等同字面量。final + 運行期賦值 (方法返回) → 無法優化,運行期 new 對象。// StringBuilder (非線程安全,高性能)
public StringBuilder append(String str) { super.append(str); return this; }
// StringBuffer (線程安全,所有公共方法 synchronized)
public synchronized StringBuffer append(String str) { super.append(str); return this; }
AbstractStringBuilder,擁有可擴容 char[]/byte[] 數組。ThreadLocal + 合併。// 源碼
String s = "";
for (int i=0; i<10000; i++) s += "java";
// 反編譯字節碼 (JDK 8+)
StringBuilder sb = new StringBuilder();
for (int i=0; i<10000; i++) {
sb.append("java");
}
s = sb.toString();
關鍵發現:
+= 被優化為 每次迭代 new StringBuilder(),極其低效!new StringBuilder() 提到循環外,僅創建一個對象,性能提升 100 倍+。| 操作 | 耗時 (ms) | 相對性能 |
|---|---|---|
| String += 循環拼接 | ~4500 | 1x (基準) |
| StringBuilder 循環 append | ~3 | 1500x |
| StringBuffer 循環 append | ~8 | 560x |
| 字面量直接相加 "a"+"b"+"c" | ~0 | 編譯期常量折疊,極快 |
| 變量間接相加 s1+s2+s3 | ~15 | 運行期 StringBuilder 優化 |
性能排序:StringBuilder > StringBuffer > String(循環拼接場景)。
intern() 複製對象到永久代。intern() 僅在常量池記錄引用,不複製對象。| 場景 | 推薦方式 | 理由 |
|---|---|---|
| 常量定義、少量拼接 | String 字面量 / + | 編譯期優化、可讀性佳 |
| 循環/大量拼接、單線程 | StringBuilder | 無鎖、可預分配容量 new StringBuilder(1024) |
| 多線程共享緩衝區 | StringBuffer / ThreadLocal | 線程安全或無鎖隔離 |
| JSON/日志序列化 | StringBuilder / 输出流 | 避免大量臨時 String 對象 |
| 字符串池去重 | String.intern() 謹慎使用 | JDK 7+ 堆常量池,防 OOM 需配置 -XX:StringTableSize |
String 不變性是基石,StringBuilder 是性能利器,StringBuffer 是歷史兼容產物。理解常量池、堆、編譯器優化三者交互,才能寫出高性能代碼、破解面試陷阱、排查生產 OOM。下次拼接字符串時,請先問自己:「這裡有循環嗎?有多線程嗎?」