使用 StringUtils.split 的坑

點贊再看,動力無限。 微信搜「 程序猿阿朗 」。

本文 Github.com/niumoo/JavaNotes未讀代碼博客 已經收錄,有很多知識點和系列文章。

在日常的 Java 開發中,由於 JDK 未能提供足夠的常用的操作類庫,通常我們會引入 Apache Commons Lang 工具庫或者 Google Guava 工具庫簡化開發過程。兩個類庫都爲 java.lang API 提供了很多實用工具,比如經常使用的字符串操作,基本數值操作、時間操作、對象反射以及併發操作等。

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
    <version>3.12.0</version>
</dependency>

但是,最近在使用 Apache Commons Lang 工具庫時踩了一個坑,導致程序出現了意料之外的結果。

StringUtils.split 的坑

也是因爲踩了這個坑,索性寫下一篇文章好好介紹下 Apache Commons Lang 工具庫中字符串操作相關 API。

先說坑是什麼,我們都知道 String 類中到的 split 方法可以分割字符串,比如字符串 aabbccdd 根據 bc 分割的結果應該是 aabcdd 纔對,這樣的結果也很容易驗證。

String str = "aabbccdd";
for (String s : str.split("bc")) {
    System.out.println(s);
}
// 結果
aab
cdd

可能是因爲 String 類中的 split 方法的影響,我一直以爲 StringUtils.split 的效果應該相同,但其實完全不同,可以試着分析下面的三個方法輸出結果是什麼,StringUtils 是 Commons Lang 類庫中的字符串工具類。

 public static void testA() {
    String str = "aabbccdd";
    String[] resultArray = StringUtils.split(str, "bc");
    for (String s : resultArray) {
        System.out.println(s);
    }
}

我對上面 testA 方法的預期是 aabcdd ,但是實際上這個方法的運行結果是:

// testA 輸出
aa
dd

可以看到 bc 字母都不見了,只剩下了 ab,這是已經發現問題了,查看源碼後發現 StringUtils.split 方法其實是按字符進行操作的,不會把分割字符串作爲一個整體來看,返回的結果中不也會包含用於分割的字符。

驗證代碼:

public static void testB() {
    String str = "abc";
    String[] resultArray = StringUtils.split(str, "ac");
    for (String s : resultArray) {
        System.out.println(s);
    }
}
// testB 輸出
b
public static void testC() {
    String str = "abcd";
    String[] resultArray = StringUtils.split(str, "ac");
    for (String s : resultArray) {
        System.out.println(s);
    }
}
// testC 輸出
b
d

輸出結果和預期的一致了。

StringUtils.split 源碼分析

點開源碼一眼看下去,發現在方法註釋中就已經進行提示了:返回的字符串數組中不包含分隔符

The separator is not included in the returned String array. Adjacent separators are treated as one separator. For more control over the split use the StrTokenizer class....

繼續追蹤源碼,可以看到最終 split 分割字符串時入參有四個。

private static String[] splitWorker(
final String str, // 原字符串 
final String separatorChars,  // 分隔符
final int max,  // 分割後返回前多少個結果,-1 爲所有
final boolean preserveAllTokens // 暫不關注
) {
}

根據分隔符的不同又分了三種情況。

1. 分隔符爲 null

final int len = str.length();
if (len == 0) {
    return ArrayUtils.EMPTY_STRING_ARRAY;
}
final List<String> list = new ArrayList<>();
int sizePlus1 = 1;
int i = 0;
int start = 0;
boolean match = false;
boolean lastMatch = false;
if (separatorChars == null) {
    // Null separator means use whitespace
    while (i < len) {
        if (Character.isWhitespace(str.charAt(i))) { 
            if (match || preserveAllTokens) {
                lastMatch = true;
                if (sizePlus1++ == max) {
                    i = len;
                    lastMatch = false;
                }
                list.add(str.substring(start, i));
                match = false;
            }
            start = ++i;
            continue;
        }
        lastMatch = false;
        match = true;
        i++;
    }
}
// ...
if (match || preserveAllTokens && lastMatch) {
            list.add(str.substring(start, i));
}

可以看到如果分隔符爲 null ,是按照空白字符 Character.isWhitespace() 分割字符串的。分割的算法邏輯爲:

a. 用於截取的開始下標置爲 0 ,逐字符讀取字符串。
b. 碰到分割的目標字符,把截取的開始下標到當前字符之前的字符串截取出來。
c. 然後用於截取的開始下標置爲下一個字符,等到下一次使用。
d. 繼續逐字符讀取字符串、

2. 分隔符爲單個字符

邏輯同上,只是判斷邏輯 Character.isWhitespace() 變爲了指定字符判斷。

// Optimise 1 character case
final char sep = separatorChars.charAt(0);
while (i < len) {
    if (str.charAt(i) == sep) { // 直接比較
      ...

3. 分隔符爲字符串

總計邏輯同上,只是判斷邏輯變爲包含判斷。

 // standard case
while (i < len) {
    if (separatorChars.indexOf(str.charAt(i)) >= 0) { // 包含判斷
        if (match || preserveAllTokens) {

如何解決?

1. 使用 splitByWholeSeparator 方法。

我們想要的是按整個字符串分割,StringUtils 工具類中已經存在具體的實現了,使用 splitByWholeSeparator 方法。

String str = "aabbccdd";
String[] resultArray = StringUtils.splitByWholeSeparator(str, "bc");
for (String s : resultArray) {
    System.out.println(s);
}
// 輸出
aab
cdd

2. 使用 Google Guava 工具庫

關於 Guava 工具庫的使用,之前也寫過一篇文章,可以參考:Guava - 拯救垃圾代碼

String str = "aabbccdd";
Iterable<String> iterable = Splitter.on("bc")
    .omitEmptyStrings() // 忽略空值
    .trimResults() // 過濾結果中的空白
    .split(str);
iterable.forEach(System.out::println);
// 輸出
aab
cdd

3. JDK String.split 方法

使用 String 中的 split 方法可以實現想要效果。

String str = "aabbccdd";
String[] res = str.split("bc");
for (String re : res) {
    System.out.println(re);
}
// 輸出
aab
cdd

但是 String 的 split 方法也有一些坑,比如下面的輸出結果。

String str = ",a,,b,";
String[] splitArr = str.split(",");
Arrays.stream(splitArr).forEach(System.out::println);
// 輸出

a

b

開頭的逗號,前出現了空格,末尾的逗號,後卻沒有空格。

一如既往,文章中代碼存放在 Github.com/niumoo/javaNotes.

<完>

文章持續更新,可以微信搜一搜「 程序猿阿朗 」或訪問「程序猿阿朗博客 」第一時間閱讀。本文 Github.com/niumoo/JavaNotes 已經收錄,有很多知識點和系列文章,歡迎Star。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章