Java 字符串 split 踩坑記

1.1 split 的坑

前幾天在公司對經過 FTP 方式上傳的數據文件按照事先規定的格式進行解析後入庫,代碼的大概實現思路是這樣的:先使用流進行文件讀取,對文件的每一行數據解析封裝成一個個對象,而後進行入庫操做。本覺得很簡單的一個操做,而後寫完代碼後本身測試發現對文件的每一行進行字符串分割的時候存在問題,在這裏作個簡單的記錄總結。在 Java 中使用 split 方法對字符串進行分割是常常使用的方法,常常在一些文本處理、字符串分割的邏輯中,須要按照必定的分隔符進行分割拆解。這樣的功能,大多數狀況下咱們都會使用 String 中的 split 方法。關於這個方法,稍不注意很容易踩坑。java

(1)split 的參數是正則表達式
首先一個常見的問題,就是忘記了 String 的 split 方法的參數不是普通的字符串,而是正則表達式,例以下面的這兩種使用方式都達不到咱們的預期:正則表達式

/**
    * @author mghio
    * @date: 2019-10-13
    * @version: 1.0
    * @description: Java 字符串 split 踩坑記
    * @since JDK 1.8
    */
    public class JavaStringSplitTests {

        @Test
        public void testStringSplitRegexArg() {
            System.out.println(Arrays.toString("m.g.h.i.o".split(".")));
            System.out.println(Arrays.toString("m|g|h|i|o".split("|")));
        }

    }

以上代碼的結果輸出爲:apache

[]
[m, |, g, |, h, |, i, |, o]

上面出錯的緣由是由於 .| 都是正則表達式,應該用轉義字符進行處理:api

"m.g.h.i.o".split("\\.")
"m|g|h|i|o".split("\\|")

在 String 類中還有其它的和這個類似的方法,例如:replaceAll。oracle

(2)split 會忽略分割後的空字符串
大多數狀況下咱們都只會使用帶一個參數的 split 方法,可是隻帶一個參數的 split 方法有個坑:就是此方法只會匹配到最後一個有值的地方,後面的會忽略掉,例如:測試

/**
    * @author mghio
    * @date: 2019-10-13
    * @version: 1.0
    * @description: Java 字符串 split 踩坑記
    * @since JDK 1.8
    */
    public class JavaStringSplitTests {
            
        @Test
        public void testStringSplitSingleArg() {
            System.out.println(Arrays.toString("m_g_h_i_o".split("_")));
            System.out.println(Arrays.toString("m_g_h_i_o__".split("_")));
            System.out.println(Arrays.toString("m__g_h_i_o_".split("_")));
        }

    }

以上代碼輸出結果爲:this

[m, g, h, i, o]
[m, g, h, i, o]
[m, , g, h, i, o]

像第2、三個輸出結果其實和咱們的預期是不符的,由於像一些文件上傳其實有的字段一般是能夠爲空的,若是使用單個參數的 split 方法進行處理就會有問題。經過查看 API 文檔 後,發現其實 String 中的 split 方法還有一個帶兩個參數的方法。第二個參數是一個整型類型變量,表明最多匹配上多少個,0 表示只匹配到最後一個有值的地方,單個參數的 split 方法的第二個參數其實就是 0,要想強制匹配能夠選擇使用負數(一般傳入 -1 ),換成如下的寫法,輸出結果就和咱們的預期一致了。google

"m_g_h_i_o".split("_", -1)      // [m, g, h, i, o]
    "m_g_h_i_o__".split("_", -1)    // [m, g, h, i, o, , ]
    "m__g_h_i_o_".split("_", -1)    // [m, , g, h, i, o, ]

(3)JDK 中字符串切割的其它 API
在 JDK 中還有一個叫作 StringTokenizer 的類也能夠對字符串進行切割,用法以下所示:編碼

/**
    * @author mghio
    * @date: 2019-10-13
    * @version: 1.0
    * @description: Java 字符串 split 踩坑記
    * @since JDK 1.8
    */
    public class JavaStringSplitTests {

    @Test
    public void testStringTokenizer() {
        StringTokenizer st = new StringTokenizer("This|is|a|mghio's|blog", "|");
        while (st.hasMoreElements()) {
        System.out.println(st.nextElement());
        }
    }

    }

不過,咱們從源碼的 javadoc 上得知,這是從 JDK 1.0 開始就已經存在了,屬於歷史遺留的類,而且推薦使用 String 的 split 方法。設計

1.2 JDK 源碼探究

經過查看 JDK 中 String 類的源碼,咱們得知在 String 類中單個參數的 split 方法(split(String regex))裏面調用了兩個參數的 split 方法(split(String regex, int limit)),兩個參數的 split 方法,先根據傳入第一個參數 regex 正則表達式分割字符串,第二個參數 limit 限定了分割後的字符串個數,超過數量限制的狀況下前limit-1個子字符串正常分割,最後一個子字符串包含剩下全部字符。單個參數的重載方法將 limit 設置爲 0。源碼以下:

public String[] split(String regex, int limit) {
        char ch = 0;
        if (((regex.value.length == 1 &&
             ".$|()[{^?*+\\".indexOf(ch = regex.charAt(0)) == -1) ||
             (regex.length() == 2 &&
              regex.charAt(0) == '\\' &&
              (((ch = regex.charAt(1))-'0')|('9'-ch)) < 0 &&
              ((ch-'a')|('z'-ch)) < 0 &&
              ((ch-'A')|('Z'-ch)) < 0)) &&
            (ch < Character.MIN_HIGH_SURROGATE ||
             ch > Character.MAX_LOW_SURROGATE))
        {
            int off = 0;
            int next = 0;
            boolean limited = limit > 0;
            ArrayList<String> list = new ArrayList<>();
            while ((next = indexOf(ch, off)) != -1) {
                if (!limited || list.size() < limit - 1) {
                    list.add(substring(off, next));
                    off = next + 1;
                } else {    // last one
                    //assert (list.size() == limit - 1);
                    list.add(substring(off, value.length));
                    off = value.length;
                    break;
                }
            }
            // If no match was found, return this
            if (off == 0)
                return new String[]{this};

            // Add remaining segment
            if (!limited || list.size() < limit)
                list.add(substring(off, value.length));

            // Construct result
            int resultSize = list.size();
            if (limit == 0) {
                while (resultSize > 0 && list.get(resultSize - 1).length() == 0) {
                    resultSize--;
                }
            }
            String[] result = new String[resultSize];
            return list.subList(0, resultSize).toArray(result);
        }
        return Pattern.compile(regex).split(this, limit);
    }

接下來讓咱們一塊兒看看 String 的 split 方法是如何實現的。

(1)特殊狀況判斷

(((regex.value.length == 1 &&
             ".$|()[{^?*+\\".indexOf(ch = regex.charAt(0)) == -1) ||
             (regex.length() == 2 &&
              regex.charAt(0) == '\\' &&
              (((ch = regex.charAt(1))-'0')|('9'-ch)) < 0 &&
              ((ch-'a')|('z'-ch)) < 0 &&
              ((ch-'A')|('Z'-ch)) < 0)) &&
            (ch < Character.MIN_HIGH_SURROGATE ||
             ch > Character.MAX_LOW_SURROGATE))
  • 第一個參數 regex 爲單個字符時,將其賦值給 ch,並判斷是否在元字符:「.$|()[{^?*+\」中
  • 第一個參數 regex 爲兩個字符時,第一個字符爲 \\(要表示一個須要用兩個\轉義獲得),第二個字符不在數字、大小寫字母和 Unicode 編碼 Character.MIN_HIGH_SURROGATE('uD800')和 Character.MAX_LOW_SURROGATE('uDBFF')之間。

(2)字符串分割
第一次分割時,使用 off 和 next,off 指向每次分割的起始位置,next 指向分隔符的下標,完成一次分割後更新 off 的值,當 list 的大小等於 limit - 1 時,直接添加剩下的子字符串。

  • 若是字符串不含有分隔符,則直接返回原字符串
  • 若是字符串進行完第一次分割後,數量沒有達到 limit - 1 的話,則剩餘的字符串在第二次添加
  • 若是傳入的第二個參數 limit 等於 0 ,則從最後的字符串往前移動,將全部的空字符串(」「)所有清除

(3)正則匹配
String 的 split 方法在不是上面的特殊狀況下,會使用兩個類 PatternMatcher 進行分割匹配處理,並且 Strig 中涉及正則的操做都是調用這兩個類進行處理的。

  • Pattern 類咱們能夠將其理解爲模式類,它主要是用來建立一個匹配模式,它的構造方法是私有的,不能直接建立該對象,能夠經過 Pattern.complie(String regex) 簡單的工廠方法建立一個正則表達式。
  • Matcher 類咱們能夠將其理解爲匹配器類,它是用來解釋 Pattern 類對字符串執行匹配操做的引擎,它的構造方法也是私有的,不能直接建立該對象,能夠經過 Pattern.matcher(CharSequence input) 方法獲得該類的實例。String 類的雙參數 split 方法最後使用 Pattern 類的 compile 和 split 方法,以下:
return Pattern.compile(regex).split(this, limit);

首先調用 Pattern 類的靜態方法 compile 獲取 Pattern 模式類對象

public static Pattern compile(String regex) {
        return new Pattern(regex, 0);
    }

接着調用 Pattern 的 split(CharSequence input, int limit) 方法,在這個方法中調 matcher(CharSequence input) 方法返回一個 Matcher 匹配器類的實例 m,與 String 類中 split 方法的特殊狀況有些相似。

  • 使用 m.find()、m.start()、m.end() 方法
  • 每找到一個分割符,則更新 start 和 end 的位置
  • 而後處理沒找到分隔符、子字符串數量小於 limit 以及 limit = 0 的狀況

1.3 其它的字符串分割方式

  • 方式一:使用 org.apache.commons.lang3.StringUtils#split,此方法使用完整的字符串做爲參數,而不是正則表達式。底層調用 splitWorker 方法(<font color="#dd0000">注意:</font>此方法會忽略分割後的空字符串)
/**
    * @author mghio
    * @date: 2019-10-13
    * @version: 1.0
    * @description: Java 字符串 split 踩坑記
    * @since JDK 1.8
    */
    public class JavaStringSplitTests {

        @Test
        public void testApacheCommonsLangStringUtils() {
            System.out.println(Arrays.toString(StringUtils.split("m.g.h.i.o", ".")));
            System.out.println(Arrays.toString(StringUtils.split("m__g_h_i_o_", "_")));
        }

    }

輸出結果:

[m, g, h, i, o]
[m, g, h, i, o]
  • 方式二:使用 com.google.common.base.Splitter,使用 Google Guava 包中提供的分割器 splitter,它提供了更加豐富的分割結果處理的方法,好比對結果先後去除空格,去除空字符串等
/**
    * @author mghio
    * @date: 2019-10-13
    * @version: 1.0
    * @description: Java 字符串 split 踩坑記
    * @since JDK 1.8
    */
    public class JavaStringSplitTests {

        @Test
        public void testApacheCommonsLangStringUtils() {
            Iterable<String> result = Splitter.on("_").split("m__g_h_i_o_");
            List<String> resultList = Lists.newArrayList();
            result.forEach(resultList::add);
            System.out.println("stringList's size: " + resultList.size());
            result.forEach(System.out::println);
        }

    }

輸出結果:

stringList's size: 7
m

g
h
i
o

1.4 總結

String 類中除了 split 方法外,有正則表達式接口的方法都是調用 Pattern(模式類)和 Matcher(匹配器類)進行實現的。JDK 源碼的每個如 finalprivate 的關鍵字都設計的十分嚴謹,多讀類和方法中的javadoc,多注意這些細節對於閱讀代碼和本身寫代碼都有很大的幫助。

相關文章
相關標籤/搜索