Java 字符串 split 的一個反直覺陷阱

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

image

異常信息如下圖所示

image

問題很簡單,我們對一個字符串執(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í)行結果

image

跟我的直覺是一致的,同樣的情況,這里返回的是 2。

PHP

在 PHP 中,我使用了 mb_split 函數(shù),該函數(shù)用于對多字節(jié)字符串進行分割

image

執(zhí)行結果如下

image

執(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í)行結果

image

同樣,"~".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í)行結果

image

Scala

然后,我又嘗試了 Scala,發(fā)現(xiàn)在 Scala 中, split 的行為有些不一樣了。

"".split("").length
"~".split("~").length
"~~".split("~").length
"".split("~").length
"~123".split("~").length
image

代碼 "~".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í)行結果

image

結果與 Scala 是一致的,同時也解釋了為什么我們會遇到 ArrayIndexOutOfBoundsException 的問題。

原因

翻閱了 Java 的 API 文檔,發(fā)現(xiàn)原來 Java 中的 split 方法確實跟其它語言是不一樣的,這一點我們特別容易忽略

image

如果分隔符表達式與字符串不匹配,則返回原始字符串作為數(shù)組的唯一值,這也就解釋了

"".split("") // 1 [""]
"".split("~") // 1 [""]

如果分隔符表單式與字符串的開始字符就已經(jīng)匹配了,則返回值中第一個元素會被設置為 ""

"~123".split("~") // 2 ["", "123"]

如果 limit 參數(shù)為 0,也就是 split(String regex) 方法,則匹配結果末尾的所有空字符串 "" 都會被丟棄,也就解釋了下面兩段代碼

"~".split("~") // 0 []
"~~".split("~") // 0 []
image

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

image

總結

在 Java 中使用字符串的 split 方法,一般情況下的行為是和其他編程語言是一致的,但在一些邊界條件下,也有一些不一致的地方,這一點是我們應該注意的,這也提醒了我們,不要想當然的認為不同語言,同名函數(shù)(方法)的功能是完全一致的,當我們遇到一些奇奇怪怪的問題時,多看官方文檔才是硬道理。

本文將會持續(xù)修正和更新,最新內容請參考我的 GITHUB 上的 程序猿成長計劃 項目,歡迎 Star,更多精彩內容請 follow me。

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容