String 是 Java 使用頻率最高、特性最獨特、優化最深入的類。它不是基本類型,卻擁有字面量語法;它是 final 類,不可繼承;它是不可變對象,卻提供豐富的「修改」方法 (實則返回新對象)。本文系統梳理 String 核心機制、常用 API、性能陷阱與正則陷阱,助你寫出正確、高效、安全的字符串代碼。
public final class String — 無法被繼承。equals()、hashCode()、compareTo() 等核心契約不被子類破壞,守護 HashMap、HashSet、安全管理器等基礎設施。private final char[] value (JDK 8) / byte[] value + coder (JDK 9+ compact strings)。substring()、concat()、replace()、toUpperCase()...) 返回新 String 對象,原對象不變。String s1 = "hello"; // 字面量:進入字符串常量池,同一文字僅一份
String s2 = new String("hello"); // new:堆上創建新對象,不進常量池 (除非調用 intern())
s1 == s2; // false (引用不同)
s1.equals(s2); // true (內容相同)
s1.intern() == s2.intern(); // true (都指向常量池同一引用)
建議:優先使用字面量,除非明確需要獨立堆對象 (極少見)。
| JDK 版本 | 常量池位置 | 影響 |
|---|---|---|
| JDK 6 及之前 | 永久代 | 受 PermGen 大小限制,intern() 易導致 OOM |
| JDK 7 | 堆 | 受堆大小管理,intern() 安全得多 |
| JDK 8+ | 堆 (元空間存類元數據) | 同 JDK 7,更穩定 |
// 場景:大量重複字符串 (如解析日志、爬蟲) 節省內存
String key = parsedString.intern(); // 將字符串放入/獲取常量池引用
map.put(key, value); // Key 共享同一引用,減少堆佔用
// 注意:intern() 返回常量池引用,若大量調用需配合 -XX:StringTableSize 调大哈希表桶數
// JDK 8 默認 60013,建議設置 -XX:StringTableSize=200003 等質數
| 方法 | 說明 | 注意 |
|---|---|---|
equals(Object) | 內容比較,區分大小寫 | ✅ 首選,== 僅比較引用 |
equalsIgnoreCase(String) | 忽略大小寫比較 | 性能略低,非熱點可用 |
compareTo(String) | 字典序比較,實現 Comparable | 返回負/0/正,用於排序 |
contains(CharSequence) | 是否包含子串 (JDK 8+) | 內部調用 indexOf |
startsWith/endsWith | 前綴/後綴匹配 | 高性能,避免正則 |
indexOf/lastIndexOf | 子串位置,-1 表無 | 支援 char/CharSequence 重載 |
String s = "abcdef";
s.substring(2); // "cdef" 從索引 2 到結尾
s.substring(2, 5); // "cde" [2,5) 左閉右開
// JDK 6 substring 共享底層 char[] (可能導致內存洩漏,大字符串截取小片段)
// JDK 7+ substring 複製新數組,安全但稍耗內存
// 分割陷阱:正則元字符
"1,2,3".split(","); // ["1","2","3"] ✅
"1|2|3".split("|"); // ["1","|","2","|","3"] ❌ | 是正則 OR
"1|2|3".split("\\|"); // ["1","2","3"] ✅ 雙重轉義: 字符串 "\\" → 正則 \| → 字面量 |
"1.2.3".split("\\."); // ["1","2","3"] ✅ . 也是元字符
// 建議:簡單分隔符用 String.split(Pattern.quote("|")) 自動轉義
s.replace("old", "new"); // 字面量替換,全量替換,非正則 (高性能)
s.replaceAll(regex, repl); // 正則替換,編譯 Pattern 開銷大
s.replaceFirst(regex, repl); // 正則替換首次匹配
s.trim(); // 去除首尾空白 (含 \t\n\r 等控制字符)
s.strip(); // JDK 11+ Unicode 感知去除空白,更完備
// ❌ 循環中用 + : 每次迭代創建新 StringBuilder + toString(),O(N²) 字符複製
String s = "";
for (int i = 0; i < 10000; i++) s += "x";
// ✅ StringBuilder: 可變字符序列,預分配容量,O(N)
StringBuilder sb = new StringBuilder(10000);
for (int i = 0; i < 10000; i++) sb.append('x');
String result = sb.toString();
// ✅ JDK 8+ StringJoiner / String.join (內部用 StringBuilder)
String joined = String.join(",", "a", "b", "c"); // "a,b,c"
// 編譯器優化:單行 + 會被編譯為 StringBuilder
String s = "a" + "b" + "c"; // 編譯時常量折疊為 "abc"
String t = "a" + var + "b"; // 運行時 new StringBuilder().append("a").append(var).append("b").toString()
| 類 | 線程安全 | 性能 | 適用場景 |
|---|---|---|---|
StringBuilder | ❌ 非線程安全 | ✅ 高 (無同步開銷) | 單線程構建字符串、方法局部變量、高性能拼接 (99% 場景) |
StringBuffer | ✅ 同步方法 | 較低 | 多線程共享緩衝區 (極少見),歷史遺留 API 兼容 |
核心方法:append()、insert()、delete()、reverse()、capacity()/ensureCapacity()。
正則元字符: . \ | ( ) [ ] { } ^ $ * + ?
Java 字符串雙重轉義: "\\." "\\|" "\\(" ...
工具方法: Pattern.quote(str) 自動為整字符串加 \Q...\E 包裹
// ❌ 每次調用編譯正則,極慢
for (String s : list) s.matches("\\d+");
// ✅ 靜態編譯一次,重用 Matcher
static final Pattern DIGITS = Pattern.compile("\\d+");
for (String s : list) DIGITS.matcher(s).matches();
// ✅ JDK 11+ Pattern.asMatchPredicate() 函數式風格
Predicate isDigits = Pattern.compile("\\d+").asMatchPredicate();
list.stream().filter(isDigits).collect(toList());
// 惡意輸入 "aaaaaaaaaaaaaaaaaaaaX" 導致指數級回溯
String evil = "(a+)+b";
// 防禦:
// 1. 使用佔有優先量詞 (a++) 或原子組 (?>a+)
// 2. 設置匹配超時 (JDK 無內建,需自定義線程/第三方庫)
// 3. 避免嵌套量詞,簡化正則邏輯
char[] (UTF-16, 2 bytes/char) 改為 byte[] + coder (LATIN1: 1 byte, UTF-16: 2 bytes)。-XX:+UseStringDeduplication
// 運行時掃描堆中相同內容的 String,讓其共享同一 byte[] 數組
// 適用於大量重複字符串場景 (日志、緩存、配置)
String 的設計哲學——不可變、共享、優化——貫穿 Java 平台演進始終。掌握常量池機制、選對拼接工具、駕馭正則轉義與預編譯、理解 Compact Strings 帶來的紅利,是寫出高質量 Java 代碼的必修課。記住:字符串看似廉價,濫用卻最貴;敬畏不可變,善用 StringBuilder。