本文源地址:http://www.fullstackyang.com/...,轉發請注明該地址或segmentfault地址,謝謝! 
最近遇到了一些字符匹配的需求,進而仔細地看了CharMatcher的源碼,發現還是有點東西值得回味,例如它為我們提供了如何在多種字符類型場景下提高靈活性從而滿足不同匹配需求的優秀示范。下面就對CharMatcher類的結構,設計模式,以及幾個算法做一些粗淺的分析。
 一、關于源碼中的彩蛋  CharMatcher類中,開頭部分有一張寵物小精靈“小火龍”的字符畫,就像本文的封面圖一樣,一開始不解為何要放一只“小火龍”在這里,后來看到其英文名Charmander才明白過來。好吧,諧音梗……略冷。
 二、類的結構和關系  下圖是CharMatcher的類關系圖,圖中藍色的是abstract類,紅色的是final類
 public abstract boolean matches(char c);public int indexIn(CharSequence sequence) // 查找匹配字符首次出現的索引位置public int countIn(CharSequence sequence) // 對匹配的字符計數public String retainFrom(CharSequence sequence) // 抽取匹配的字符public CharMatcher negate() // 取反public CharMatcher and(CharMatcher other) // 與public CharMatcher or(CharMatcher other) // 或... 
如上圖所示,CharMatcher類有很多的子類,一部分是直接繼承于父類,一部分是繼承于FastMatcher,另外還有繼承于Negated和RangeMatcher。子類通過實現matches方法或重寫其他父類的方法,從而提供了各種不同的具體操作,如Is(判斷是否為某一個字符),Digit(判斷是否為數字字符),Ascii(判斷是否為ASCII字符)等。
 三、設計模式  上一節說到CharMatcher提供很多子類,為了較好地管理和使用這些類,CharMatcher對外提供了基于內部類的靜態工廠方法或者單例模式來獲得某個實例,舉例來說:
 public static CharMatcher is(final char match) {return new Is(match);
}private static final class Is extends FastMatcher {private final char match;Is(char match) {this.match = match;}...} 使用靜態工廠方法的好處,這點在《Effective Java》一書中有詳細的介紹 
public static CharMatcher ascii() {return Ascii.INSTANCE;
}private static final class Ascii extends NamedFastMatcher {static final Ascii INSTANCE = new Ascii();Ascii() {super("CharMatcher.ascii()");}...
} 
這樣我們就可以很方便地獲得一個實例,并對相應的字符類型做處理,比如抽取字符串中所有的數字
 CharMatcher.inRange('0', '9').retainFrom("abc12d34ef");
// 當然也可以用Digit類,不過最近的版本已經被標記為Deprecated
// 區別在于Digit類處理了字符0到9的各種unicode碼,不過大多數情況還是處理ASCII數字,所以建議使用inRange
CharMatcher.digit().retainFrom("abc12d34ef");
// 1234 
當然也可以通過negate/or/and產生一些復雜的組合:
 CharMatcher.inRange('0','9').or(CharMatcher.is('d')).retainFrom("abc12d34ef");
// 12d34 
另外還有一個ForPredicate的子類,它接收Predicate對象作為參數,然后用Predicate的apply方法來實現matches方法,這樣就用lamda表達式創建一些實例了,例如:
 CharMatcher.inRange('0', '9').or(CharMatcher.is('d')).or(CharMatcher.forPredicate(c -> c <= 'b' || c > 'e')).retainFrom("abc12d34ef");
// ab12d34f 
四、算法分析  collapseFrom方法,如代碼注釋所示,把一個字符串中匹配到的(連續)部分替換為給定的字符, //CharMatcher.anyOf("eko").collapseFrom("bookkeeper", '-') returns "b-p-r"
public String collapseFrom(CharSequence sequence, char replacement) {// This implementation avoids unnecessary allocation.int len = sequence.length();for (int i = 0; i < len; i++) {char c = sequence.charAt(i);if (matches(c)) {if (c == replacement && (i == len - 1 || !matches(sequence.charAt(i + 1)))) {// a no-op replacementi++;} else {StringBuilder builder = new StringBuilder(len).append(sequence, 0, i).append(replacement);return finishCollapseFrom(sequence, i + 1, len, replacement, builder, true);}}}// no replacement neededreturn sequence.toString();
}private String finishCollapseFrom(CharSequence sequence,int start,int end,char replacement,StringBuilder builder,boolean inMatchingGroup) {for (int i = start; i < end; i++) {char c = sequence.charAt(i);if (matches(c)) {if (!inMatchingGroup) {builder.append(replacement);inMatchingGroup = true;}} else {builder.append(c);inMatchingGroup = false;}}return builder.toString();
}  事實上,CharMatcher里面的算法基本上都和這個差不多程度。 
正如注釋部分所述,這個算法沒有分配不必要的空間。遍歷過程中當發現當前字符滿足匹配條件,這時再做一次判斷,如果當前字符本身就是所需要替換的字符replacement,那么這種情況是不需要進行替換操作(感覺可以直接用一個if(c != replacement)換掉else,并不需要i++的操作),否則將i之前的字符拼上replacement形成一個“半成品”傳入finishCollapseFrom,在該方法中利用了一個布爾值inMatchingGroup來控制是否需要拼接replacement,當發現滿足匹配條件時,再檢查inMatchingGroup是否為false,它表示上一輪拼接的不是replacement,以保證返回的結果中不會出現兩個以上連續的replacement。
 Whitespace.matches 即判斷該字符是否為空白字符,包括空格,換行等 static final class Whitespace extends NamedFastMatcher {static final String TABLE ="\u2002\u3000\r\u0085\u200A\u2005\u2000\u3000"+ "\u2029\u000B\u3000\u2008\u2003\u205F\u3000\u1680"+ "\u0009\u0020\u2006\u2001\u202F\u00A0\u000C\u2009"+ "\u3000\u2004\u3000\u3000\u2028\n\u2007\u3000";static final int MULTIPLIER = 1682554634;static final int SHIFT = Integer.numberOfLeadingZeros(TABLE.length() - 1);static final Whitespace INSTANCE = new Whitespace();@Overridepublic boolean matches(char c) {return TABLE.charAt((MULTIPLIER * c) >>> SHIFT) == c;}
} 
這個算法本身很簡單,即TABLE字符串中是否存在同樣的字符c,巧妙的是它的定位方式。
     @Testpublic void test() {// 去掉table中重復的字符String WHITE = "\u2002\r\u0085\u200A\u2005\u2000"+ "\u2029\u000B\u2008\u2003\u205F\u1680"+ "\u0009\u0020\u2006\u2001\u202F\u00A0\u000C\u2009"+ "\u2004\u2028\n\u2007\u3000";char[] chars = WHITE.toCharArray();char filler = chars[chars.length - 1];char[] table = new char[32];int shift = Integer.numberOfLeadingZeros(WHITE.length());for (int i = 0; i <= Integer.MAX_VALUE; i++) {Arrays.fill(table, filler);//先用最后一個字符填充整個tableboolean conflict = false;for (char c : chars) {int index = (i * c) >>> shift;//如果當前字符為填充字符,則覆蓋填充字符,否則跳過if (table[index] != filler) { conflict = true;continue;}table[index] = c;}if (conflict)continue;System.out.println("MULTIPLIER: " + i);System.out.println("TABLE:" + new String(table));}} 
上面可以得到多種MULTIPLIER和TABLE的結果。當然,反推過程比較簡單粗暴,一定有更優雅更高效的實現方式。不過這里想要表達的是,它本身是一個簡單的查找算法,通常的復雜度為O(logn),這里巧妙通過映射函數,將字符映射為字符串下標索引,使得時間復雜度為O(1),不得不佩服Guava開發者們追求極致的精神。
 removeFrom方法,即在給定字符串中,刪除其匹配的部分 // CharMatcher.is('a').removeFrom("bazaar") returns "bzr"
public String removeFrom(CharSequence sequence) {String string = sequence.toString();int pos = indexIn(string);if (pos == -1) {return string;}char[] chars = string.toCharArray();int spread = 1;// This unusual loop comes from extensive benchmarkingOUT:while (true) {pos++;while (true) {if (pos == chars.length) {break OUT;}if (matches(chars[pos])) {break;}chars[pos - spread] = chars[pos];pos++;}spread++;}return new String(chars, 0, pos - spread);} 
比較詭異的是,它使用了兩層while循環,以及break [lable]的語法(這種用法并不多見,可以理解為goto語句的改良形式,可以方便地跳出多層循環),不過在內層循環時同樣也做了pos++的操作,本質上還是O(n)的時間復雜度,算法思想是char數組的位移操作,每次匹配到一個字符時,spread就自增,其他情況則每個數組元素向前移動,具體來說,spread的作用相當于對匹配到的字符進行計數,匹配到1個元素,pos指向的元素及其之后的元素向前移動1步以覆蓋掉上一輪命中的字符,匹配到2個元素,pos執行的元素及其之后的元素向前移動2步,以覆蓋上一次移動留下的空位和上一輪命中的字符,依次類推。最終利用String的構造函數(第二個參數是offset,即初始的偏移位置,第三個參數count,即所需長度)返回正確的字符串。
 五、其他  在CharMatcher羅列多種字符的不同Unicode碼,如果你在其他的工作場景下需要用的這些unicode,可以參考一下CharMatcher。
 private static final String ZEROES ="0\u0660\u06f0\u07c0\u0966\u09e6\u0a66\u0ae6\u0b66\u0be6\u0c66\u0ce6\u0d66\u0de6"+ "\u0e50\u0ed0\u0f20\u1040\u1090\u17e0\u1810\u1946\u19d0\u1a80\u1a90\u1b50\u1bb0"+ "\u1c40\u1c50\ua620\ua8d0\ua900\ua9d0\ua9f0\uaa50\uabf0\uff10"; 如果要獲得其他數字的unicode,就直接對應加上對應的數值 
static final String TABLE ="\u2002\u3000\r\u0085\u200A\u2005\u2000\u3000"+ "\u2029\u000B\u3000\u2008\u2003\u205F\u3000\u1680"+ "\u0009\u0020\u2006\u2001\u202F\u00A0\u000C\u2009"+ "\u3000\u2004\u3000\u3000\u2028\n\u2007\u3000"; 
private static final String RANGE_STARTS ="\u0000\u007f\u00ad\u0600\u061c\u06dd\u070f\u08e2\u1680\u180e\u2000\u2028\u205f\u2066"+ "\u3000\ud800\ufeff\ufff9";
private static final String RANGE_ENDS = // inclusive ends"\u0020\u00a0\u00ad\u0605\u061c\u06dd\u070f\u08e2\u1680\u180e\u200f\u202f\u2064\u206f"+ "\u3000\uf8ff\ufeff\ufffb"; 
"\u0000\u05be\u05d0\u05f3\u0600\u0750\u0e00\u1e00\u2100\ufb50\ufe70\uff61"
"\u04f9\u05be\u05ea\u05f4\u06ff\u077f\u0e7f\u20af\u213a\ufdff\ufeff\uffdc" 中文字符就是雙字節長度
                            
總結 
                            
                                以上是生活随笔 為你收集整理的[轮子系列]Google Guava之CharMatcher源码分析 的全部內容,希望文章能夠幫你解決所遇到的問題。
                            
                            
                                如果覺得生活随笔 網站內容還不錯,歡迎將生活随笔 推薦給好友。