Java String.split() 深度解析:為什麼「a,b,c,」分割長度是 3 不是 6?
String.split() 是日常開發最高頻 API 之一,但其 limit 參數行為、尾部空串丟棄機制、正則元字元轉義等細節極易踩坑。本文通過案例、源碼、Javadoc 深度剖析,建立完整心智模型。
核心現象重現
String str1 = "a,b,c,,,a";
String str2 = "a,b,c,,,";
String str3 = "a,b,c, , ,";
str1.split(",") // [a, b, c, , , a] 長度 6
str2.split(",") // [a, b, c] 長度 3 ❌ 尾部空串被丟棄!
str3.split(",") // [a, b,c, , , ] 長度 6 (空格不是空串)
str2.split(",", -1) // [a, b, c, , , ] 長度 6 ✅ 保留尾部空串
str2.split(",", 5) // [a, b, c, , ,] 長度 5 (限制最多 5 段)
兩個重載方法簽名與契約
// 1. 單參數版:等價於 split(regex, 0)
public String[] split(String regex)
// 2. 雙參數版:limit 控制分割次數與結果長度
public String[] split(String regex, int limit)
limit 參數完整語義表
| limit 值 | 行為 | 結果長度上限 | 尾部空串 |
| limit > 0 | 模式最多應用 limit-1 次 | ≤ limit | 保留 |
| limit = 0 | 模式應用盡可能多次 | 無限制 | 丟棄 |
| limit < 0 | 模式應用盡可能多次 | 無限制 | 保留 |
官方示例驗證 (JDK 17)
"boo:and:foo".split(":", 2) // [boo, and:foo] limit=2, 最多1次分割
"boo:and:foo".split(":", 5) // [boo, and, foo] limit=5, 最多4次分割
"boo:and:foo".split(":", -2) // [boo, and, foo] 負數保留尾部空串
"boo:and:foo".split(":", 0) // [boo, and, foo] 默認丟棄尾部空串
"b o o".split("o", 5) // [b, , , , ] 保留中間空串
"b o o".split("o", -2) // [b, , , , ] 同上
"b o o".split("o", 0) // [b, , ] 丟棄尾部空串
源碼級原理 (OpenJDK 17)
public String[] split(String regex, int limit) {
// 內部委託 Pattern.split()
return Pattern.compile(regex).split(this, limit);
}
// Pattern.split() 核心邏輯簡化:
// 1. 使用 Matcher 尋找所有匹配位置
// 2. 根據 limit 計算實際分割次數
// 3. 截取子串填充結果數組
// 4. limit <= 0 時保留尾部空串,limit == 0 時最後再 trim 尾部空串
常見正則陷阱與轉義
| 字符 | 正則含義 | 分割需轉義 | 代碼寫法 |
. | 任意字符 | ✅ | split("\\.") |
| | 或 | ✅ | split("\\|") |
* + ? | 量詞 | ✅ | split("\\*") |
( ) [ ] { } | 分組/字符類 | ✅ | split("\\)") |
^ $ | 行首/行尾 | ✅ | split("\\^") |
\ | 轉義 | ✅ | split("\\\\") |
實戰最佳實踐模式
模式一:CSV 風格分割 (保留尾部空列)
// 正確:負數 limit 保留尾部空串
String[] fields = csvLine.split(",", -1);
// 進階:處理引號包裹、轉義 (建議用 Apache Commons CSV 或 OpenCSV)
模式二:多空白分割 (正則 \s+)
// 分割任意空白字符 (空格、制表、換行)
String[] words = line.trim().split("\\s+");
// 等價於 split("[ \\t\\n\\r\\f]+")
模式三:固定分隔符字符串 (非正則)
// 純字符串分割,無正則開銷
String[] parts = "a|b|c".split(Pattern.quote("|"));
// 或 Java 12+
// String[] parts = "a|b|c".split("\\Q|\\E"); // \Q...\E 字面量引用
模式四:高性能場景避免 split
// 熱點路徑:手寫解析器、StringTokenizer (舊)、索引查找
// 基準測試:split() 比手寫慢 3-5x (正則編譯、Matcher 創建、數組分配)
public static List fastSplit(String s, char delim) {
List res = new ArrayList<>();
int start = 0;
for (int i = 0; i < s.length(); i++) {
if (s.charAt(i) == delim) {
res.add(s.substring(start, i));
start = i + 1;
}
}
res.add(s.substring(start)); // 最後一段
return res;
}
邊界情況測試矩陣
// 空字符串
"".split(",") // [""] 長度 1
"".split(",", -1) // [""] 長度 1
// 無分隔符
"abc".split(",") // ["abc"] 長度 1
// 全分隔符
",,,".split(",") // [] 長度 0 (limit=0 丟棄所有)
",,,".split(",", -1) // ["", "", "", ""] 長度 4
// 正則特殊字符
"a.b".split(".") // [] 錯誤!. 匹配任意字符
"a.b".split("\\.") // ["a", "b"] 正確
遷移建議:Java 12+ 新 API
// String.splitWithDelimiters() 保留分隔符 (預覽特性)
// Pattern.splitAsStream() 返回 Stream (Java 11+)
Stream stream = Pattern.compile(",").splitAsStream("a,b,c");
// 配合 limit 需自行控制
總結速查卡
- 默認行為:
split(regex) = split(regex, 0) → 丟棄尾部空串
- 保留尾部空串:必須用
split(regex, -1) (負數)
- 限制分段數:
split(regex, n) (n>0),最多 n 段
- 正則元字元:
. | * + ? ( ) [ ] { } ^ $ \ 必須雙重轉義 \\.
- 性能敏感:避免循環中重複編譯 Pattern,預編譯或手寫解析
- CSV/複雜格式:不要用 split,用專業解析庫