一、問題
java中String的split()是我們經常使用的方法,用來按照特定字符分割字符串,那么我們看以下一段代碼:
public void splitTest() { String str = "aaa|bbb|ccc"; String[] array = str.split("|"); System.out.println(Arrays.toString(array)); }
是不是感覺很簡單,就是吧str按照"|"分割,結果就是[aaa,bbb,ccc]嘛。如果你這么想,那么以后在用這個方法時你可能會犯下大錯,把程序跑起來,你會驚訝的發現程序輸入如下結果:
[, a, a, a, |, b, b, b, |, c, c, c]
為什么會出現這種情況呢?我們再來運行一下str.split("");就會發現結果和之前的結果一樣,也就是說我們使用"|"分割的時候其實split是按照空字符分割的。
二、探究
為了找到原因,我們在來使用其他幾種字符測試一下,結果如下:
0:[aaa, bbb, ccc]
$:[aaa$bbb$ccc]
,:[aaa, bbb, ccc]
*:異常java.util.regex.PatternSyntaxException: Dangling meta character '*' near index 0
^:[aaa^bbb^ccc]
我們會發現對於數字,","結果是正確的,而對於特殊字符,結果都不正確,而且如果經驗豐富,你可能已經發現,這些特殊字符都是正則表達式中的匹配符,而那個異常也很明確的說明了這一點。
因此,我們可以猜測在split內部使用了正則表達式來匹配並分割字符串。那么現在又有一個疑問:我們知道一般情況下String涉及的字符串處理多數使用indexOf()(可能是考慮效率問題吧),那么這里為什么使用正則呢?我們還是直接看看源碼吧:
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); }
可以看到方法內有一個if,如果條件為true,那么就使用indexOf()判斷后substring()截取,如果為false,則使用正則處理。那么我們就來分析下這個if的條件:
//第一步部分:當regex的長度為1且不是“.$|()[{^?*+\\”中的時,為真 (regex.value.length == 1 &&".$|()[{^?*+\\".indexOf(ch = regex.charAt(0)) == -1) //第二部分:當長度為2時且第一個字符為“\”轉義字符,第二個字符不是字符0-9 a-z A-Z 以及utf-16之間的字符 (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可以看出如果regex內容為一個非正則匹配符或者是轉以后的特殊字符時,采用indexOf()+substring()處理,否則使用正則表達式。
那么為什么這么做呢,直接使用第一種方法不就行了?其實我們可以考慮一種復雜的情況:
aaax111xbbbx222xcccx333xddd
如果我想分割出這樣的結果 [aaa, bbb, ccc, ddd] 應該則么做呢?按照第一種方法,實現起來很麻煩,需要一大堆判斷,反而不如正則方便,而String中的split()方法正是出於這樣的考慮實現的,不信你用split("x.*?x")試試。
三、啟示
這次這件事雖小,但是卻讓我收獲不少。
- 1、使用一個不了解的方法前,一定要看一眼提示,split方法說明里雖然沒有明確的提示上述問題,但是多個地方都提到了正則,連參數名字都是regex,作為一個經驗豐富的程序員,應該想到這一點。
- 2、對於一些小的功能點(往往我們還胸有成竹)一定要寫個Test測一下,其實把代碼貼過去跑一下費不了多少勁,但是當他淹沒在成百上千行的代碼中時,想要在發現問題就很費勁了,特別是你不調試根本想不到這點會錯。
- 3、我們在多數項目中都要建個util工具包,封裝各種輔助的操作類,然而這些類的功能往往只是滿足當時的需求,之后用到類似但有變動的功能時往往習慣單獨實現一個,而不是實現一個完善的覆蓋原來的,從設計的角度會造成接口不一致:明明同樣的需求、同樣的參數,我卻還要看看他的內部實現才能選擇用哪個。對於這些工具類,我們不一定要非常完美,但是應該用心做到盡量完善,這也方便復用。
扯這么多,也不知道說了些什么,OVER,睡覺......
