最近生產環(huán)境遇到一個奇怪的數(shù)組下標越界報錯,如下圖代碼所示,我們可以肯定的是 fieldName 變量不為空(不是空字符串,也不是 null),但是代碼執(zhí)行到讀取 names[0] 變量的時候,拋出了一個 數(shù)組下標越界 (java.lang.ArrayIndexOutOfBoundsException) 的異常。
異常信息如下圖所示

問題很簡單,我們對一個字符串執(zhí)行 split 方法之后,以過往其它編程語言(Go、PHP、Javascript、Dart 等)的使用經(jīng)驗來看,即使字符串為空,即使沒有匹配到分隔符,在返回值數(shù)組中也會包含一個當前字符串的值。但是這里卻拋出了 ArrayIndexOutOfBoundsException,難道 split 方法的返回值可能為空數(shù)組?
最終經(jīng)過排查發(fā)現(xiàn),在上述代碼段中,當 fieldName 的值為 "~" 的時候,我們訪問 names[0] 就會拋出 ArrayIndexOutOfBoundsException,為什么會這樣呢?
本文將會持續(xù)修正和更新,最新內容請參考我的 GITHUB 上的 程序猿成長計劃 項目,歡迎 Star,更多精彩內容請 follow me。
問題
在 Java 中,如果執(zhí)行下面這段代碼,直覺上你認為會輸出什么?
String str = "~";
String []arr = str.split("~");
System.out.println(arr.length);
如果你有其他編程語言的經(jīng)驗,可能直覺上會覺得這里輸出的應該是 2,但是遺憾的是,這里輸出的是 0,變量 arr 是個空數(shù)組。
這里不禁懷疑自己之前的記憶是不是有偏差,于是我又使用其它語言來嘗試復現(xiàn)這個問題。
不同語言中 split 的行為
我總結了一個表格,說明了不用語言不同的行為,這里對比的是執(zhí)行 split 函數(shù)/方法后返回數(shù)組的長度:
| 語言\函數(shù) | "".split("") |
"~".split("~") |
"~~".split("~") |
"".split("~") |
"~123".split("~") |
|---|---|---|---|---|---|
| Javascript | 0 | 2 | 3 | 1 | 2 |
| PHP | 0 | 2 | 3 | 1 | 2 |
| Dart | 0 | 2 | 3 | 1 | 2 |
| Golang | 0 | 2 | 3 | 1 | 2 |
| Scala | 1 | 0 | 0 | 1 | 2 |
| Java | 1 | 0 | 0 | 1 | 2 |
Javascript
首先是 Javascript,在瀏覽器的控制臺上直接執(zhí)行,得到了下面的結果
"".split("")
"~".split("~")
"~~".split("~")
"".split("~")
"~123".split("~")
執(zhí)行結果
跟我的直覺是一致的,同樣的情況,這里返回的是 2。
PHP
在 PHP 中,我使用了 mb_split 函數(shù),該函數(shù)用于對多字節(jié)字符串進行分割

執(zhí)行結果如下
執(zhí)行結果跟我的直覺也是一致的,同樣的情況,這里返回的是 2。
Dart
然后是 Google 的 Dart,這是一門主要用于使用 Flutter 來開發(fā)跨平臺應用的編程語言,代碼如下
void main() {
print("".split('').length); // 0
print("~".split('~').length); // 2
print("~~".split('~').length); // 3
print("".split('~').length); // 1
print("~123".split('~').length); // 2
}
執(zhí)行結果

同樣,"~".split("~") 也是返回了兩個值。
Golang
在 Golang 中,執(zhí)行結果依舊是符合直覺的,返回的是 2。
package main
import(
"strings"
"fmt"
)
func main() {
printStrs(strings.Split("", "")) // 0 []
printStrs(strings.Split("~", "~")) // 2 ["", "", ]
printStrs(strings.Split("~~", "~")) // 3 ["", "", "", ]
printStrs(strings.Split("", "~")) // 1 ["", ]
printStrs(strings.Split("~123", "~")) // 2 ["", "123", ]
}
func printStrs(s []string) {
fmt.Print(len(s), " [")
for _, item := range s {
fmt.Printf(`"%s", `, item)
}
fmt.Print("]\n")
}
執(zhí)行結果

Scala
然后,我又嘗試了 Scala,發(fā)現(xiàn)在 Scala 中, split 的行為有些不一樣了。
"".split("").length
"~".split("~").length
"~~".split("~").length
"".split("~").length
"~123".split("~").length
代碼 "~".split("~") 返回的是 空數(shù)組,與在 Java 中我們遇到的問題如出一轍。
Java
最后,我又用 Java 執(zhí)行了同樣的代碼
package example;
import org.junit.Test;
public class ExampleTest {
@Test
public void testSplit() {
printStrings("".split("")); // 1 ["", ]
printStrings("~".split("~")); // 0 []
printStrings("~~".split("~")); // 0 []
printStrings("".split("~")); // 1 ["", ]
printStrings("~123".split("~")); // 2 ["", "123", ]
}
private void printStrings(String[] strings) {
System.out.print(strings.length + " [");
for (String str : strings) {
System.out.printf("\"%s\", ", str);
}
System.out.println("]");
}
}
執(zhí)行結果
結果與 Scala 是一致的,同時也解釋了為什么我們會遇到 ArrayIndexOutOfBoundsException 的問題。
原因
翻閱了 Java 的 API 文檔,發(fā)現(xiàn)原來 Java 中的 split 方法確實跟其它語言是不一樣的,這一點我們特別容易忽略

如果分隔符表達式與字符串不匹配,則返回原始字符串作為數(shù)組的唯一值,這也就解釋了
"".split("") // 1 [""]
"".split("~") // 1 [""]
如果分隔符表單式與字符串的開始字符就已經(jīng)匹配了,則返回值中第一個元素會被設置為 ""
"~123".split("~") // 2 ["", "123"]
如果 limit 參數(shù)為 0,也就是 split(String regex) 方法,則匹配結果末尾的所有空字符串 "" 都會被丟棄,也就解釋了下面兩段代碼
"~".split("~") // 0 []
"~~".split("~") // 0 []

然后我又翻閱了 Scala 的官方文檔,Scala 和 Java 的行為是一致的。

總結
在 Java 中使用字符串的 split 方法,一般情況下的行為是和其他編程語言是一致的,但在一些邊界條件下,也有一些不一致的地方,這一點是我們應該注意的,這也提醒了我們,不要想當然的認為不同語言,同名函數(shù)(方法)的功能是完全一致的,當我們遇到一些奇奇怪怪的問題時,多看官方文檔才是硬道理。
本文將會持續(xù)修正和更新,最新內容請參考我的 GITHUB 上的 程序猿成長計劃 項目,歡迎 Star,更多精彩內容請 follow me。