split() 函式解析 (一)

居合子發表於2019-03-04

起源

突然研究split()函式是有一定原因的,昨天晚上有個厲害的學長在實驗室的群裡拋了這樣一個問題:

假設存在一個陣列 array ={“AB”, “12”},還存在一個字串string = abcAB0123,有一個函式f(String s),
使得 {“abc”, “AB”, “0”, “12”, “3”} == f(string)。也就是把string按array中的元素拆分。求解這個·f函式。
ps:string.split(“AB|12″)的到的結果是{”abc”, “0”, “3”},不滿足條件。

既然string.split()無法實現,那麼看一下split()的原始碼是如何實現的,對其進行改造就可以了。

原始碼追蹤

首先是String.split(String regex),它的實現是這樣的:

public String[] split(String regex) {
    return split(regex, 0);
}複製程式碼

繼續追蹤,看一下String.split(String regex, int limit)的內部是如何實現的:

/**
* @param  regex
*         the delimiting regular expression
* 這裡的regex可以是一個正規表示式
* @param  limit
*         the result threshold, as described above
* 限制切割字串的段數
* @return  the array of strings computed by splitting this string
*          around matches of the given regular expression
*
* @throws  PatternSyntaxException
*          if the regular expression`s syntax is invalid
*/
public String[] split(String regex, int limit) {
    /* fastpath if the regex is a
     (1)one-char String and this character is not one of the
        RegEx`s meta characters ".$|()[{^?*+\", or
     (2)two-char String and the first char is the backslash and
        the second is not the ascii digit or ascii letter.
     */
    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;  //判斷是否有限制,如果limit = 0則表示無限制
        ArrayList<String> list = new ArrayList<>(); //盛裝切割之後的字串
        while ((next = indexOf(ch, off)) != -1) { //offset之後還有該字元
            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);
}複製程式碼

首先看一下函式第一行的註釋:

fastpath if the regex is a
(1)one-char String and this character is not one of the
RegEx`s meta characters “.$|()[{^?*+”, or
(2)two-char String and the first char is the backslash and
the second is not the ascii digit or ascii letter.

翻譯過來就是:

  1. 當regex是一個字元的字串並且這個字元不是正規表示式中的元字元或者

  2. regex是兩個字元的字串並且第一個字元是`/`,第二個字元不是數字或者字母的時候(其實這裡也相當於一個字元,使一些轉義字元)

    本函式的時間複雜度會很低(fastpath)?

那麼對應的程式碼就是:

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))複製程式碼

上面的這個if語句就是是否符合fastpath的條件:

  • (regex.value.length == 1 && ".$|()[{^?*+\".indexOf(ch = regex.charAt(0)) == -1) 長度為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) 長度為2,第一個字元是“ 並且第二個字元不是字母或者數字。((ch-`A`)|(`Z`-ch)) < 0這個判斷是否為字元的方式很nice,很獨特!

  • (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迴圈:while ((next = indexOf(ch, off)) != -1)

出現了一個indexOf(int ch, int fromIndex)方法,這個方法的英文解釋是這樣的:

Returns the index within this string of the first occurrence of the specified character, starting the search at the specified index.

翻譯一下就是:返回目標字元在字串中fromIndex位置之後第一次 出現的位置,如果沒有的話就返回-1。

所以這個while迴圈就是當剩下的字串還有目標字元的話,就會繼續迴圈。

接下來的部分就簡單多了,根據offset和目標出現的下一個位置使用substring函式對字串進行切割,並把切割下的部分新增到list中去,如果這是目標的最後一次出現位置或者超出limit的範圍,直接把字串最後的部分新增到list中。注意,每次迴圈都要調整偏移量,如果不是最後一次迴圈,令off = next + 1。

迴圈結束之後,如果off仍然是0,說明沒有匹配到,直接返回就好。然後把list轉換為字串陣列返回就可以了。

更加深入

你以為就這樣結束了?

別忘了,我們上面討論的只是fastpath情況,也就是用一個字元切割字串。

Pattern.compile(regex).split(this, limit) 這是另一種情況,請等待我的下回講解。

PS:那位學長已經解決了這個那個問題,方法是:使用構詞器

相關文章