首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 綜合教學

Java 字符串核心三劍客:String、StringBuilder、StringBuffer 深度解析與性能實測

     編輯:Java 綜合教學

Java 字符串核心三劍客:String、StringBuilder、StringBuffer 深度解析與性能實測

String 是 Java 使用頻率最高的類,面試必考、業務必用。本文從源碼層面剖析 String 不變性本質,對比 StringBuilder/StringBuffer 異同,通過字節碼反編譯與基準測試揭示拼接性能真相,並破解流傳已久的「創建對象數量」面試陷阱。

一、String 不變性源碼鐵證

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);
    }
}

核心結論

  • final 類、final 數組、final 偏移/長度:結構層面保證不可變。
  • 任何修改操作 (substring/concat/replace/trim) 均返回新對象,原對象不變。
  • 不可變帶來的紅利:線程安全、HashMap Key 安全、字符串常量池共享、安全敏感參數傳遞不可竄改。

二、字面量 vs new String():常量池與堆的博弈

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 變量編譯期優化

final String a = "hello";
String b = a + "world";  // 編譯期直接優化為 "helloworld" 常量
// 等價於 String b = "helloworld";
  • final + 編譯期常量值 → 編譯期常量折疊,等同字面量。
  • final + 運行期賦值 (方法返回) → 無法優化,運行期 new 對象。

三、StringBuilder vs StringBuffer:是否同步的區別

// 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[] 數組。
  • 單線程/線程封閉場景必用 StringBuilder,避免鎖開銷。
  • 多線程共享緩衝區才用 StringBuffer,或改用 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 倍+。
  • 結論:循環/大量拼接必須手動使用 StringBuilder,不要依賴編譯器優化。

五、基準測試實測 (JDK 8, 50000 次循環)

操作耗時 (ms)相對性能
String += 循環拼接~45001x (基準)
StringBuilder 循環 append~31500x
StringBuffer 循環 append~8560x
字面量直接相加 "a"+"b"+"c"~0編譯期常量折疊,極快
變量間接相加 s1+s2+s3~15運行期 StringBuilder 優化

性能排序:StringBuilder > StringBuffer > String(循環拼接場景)。

六、面試高頻陷阱題破解

  1. String s = new String("abc") 創建幾個對象?
    • 運行期 1 個 (堆對象);類加載期常量池 1 個 ("abc")。面試建議反問:「是指運行期 new 操作,還是包含類加載?」
  2. s1 = "a" + "b" vs s1 = s1 + "a" + "b" 效率差異?
    • 前者編譯期優化為 "ab",1 次 append;後者運行期 2 次 append。字節碼可見。
  3. intern() 行為變化 (JDK 6 vs 7+)
    • JDK 6:常量池在永久代,intern() 複製對象到永久代。
    • JDK 7+:常量池移至堆,intern() 僅在常量池記錄引用,不複製對象。

七、最佳實踐速查表

場景推薦方式理由
常量定義、少量拼接String 字面量 / +編譯期優化、可讀性佳
循環/大量拼接、單線程StringBuilder無鎖、可預分配容量 new StringBuilder(1024)
多線程共享緩衝區StringBuffer / ThreadLocal線程安全或無鎖隔離
JSON/日志序列化StringBuilder / 输出流避免大量臨時 String 對象
字符串池去重String.intern() 謹慎使用JDK 7+ 堆常量池,防 OOM 需配置 -XX:StringTableSize

總結

String 不變性是基石,StringBuilder 是性能利器,StringBuffer 是歷史兼容產物。理解常量池、堆、編譯器優化三者交互,才能寫出高性能代碼、破解面試陷阱、排查生產 OOM。下次拼接字符串時,請先問自己:「這裡有循環嗎?有多線程嗎?」

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved