天天看點

Guava 是個風火輪之基礎工具(2)前言Splitter

<a></a>

guava 提供了 joiner 類用于将多個對象拼接成字元串,如果我們需要一個反向的操作,就要用到 splitter 類。splitter 能夠将一個字元串按照指定的分隔符拆分成可疊代周遊的字元串集合,<code>iterable&lt;string&gt;</code>。

splitter 的 api 和 joiner 類似,使用 splitter#on 指定分隔符,使用 splitter#split 完成拆分。

splitter 還支援使用正規表達式來描述分隔符。

splitter 還支援根據長度來拆分字元串。

與 joiner.mapjoiner 相對,splitter.mapsplitter 用來拆分被拼接了的 map 對象,傳回 <code>map&lt;string, string&gt;</code>。

需要注意的是,不是所有由 mapjoiner 拼接出來的字元串,都能夠被 mapsplitter 拆分,mapsplitter 對鍵值對個格式有着嚴格的校驗。比如下面的拆分會抛出異常。

是以,如果希望使用 mapsplitter 來拆分 kv 結構的字元串,需要保證鍵-值分隔符和鍵值對之間的分隔符不會稱為鍵或值的一部分。也許是出于類似方面的考慮,mapsplitter 被加上了 @beta 注解,也許在不久的将來它會被移除,或者有大的變化。如果在應用中有可能用到 kv 結構的字元串,我一般推薦使用 json 而不是 mapjoiner + mapsplitter。

源碼來自 guava 18.0。splitter 類源碼約 600 行,依舊大部分是注釋和函數重載。splitter 的實作中有十分明顯的政策模式和模闆模式,有各種神乎其技的方法覆寫,還有 guava 久負盛名的疊代技巧和惰性計算。

不得不說,平時翻閱一些基礎類庫,總是感覺 “這種代碼我也能寫”,“這代碼寫的還沒我好”,“在工具類中強依賴日志元件,人幹事?”,如果 ide 配上彈幕恐怕全是吐槽,難有讓人精神為之一振的代碼。閱讀 guava 的代碼,每次都有新的驚喜,各種神技巧黑科技讓我五體投地,寫代碼的腦洞半徑屢次被 guava 撐大。

splitter 類有 4 個成員變量,strategy 用于幫助實作政策模式,omitemptystrings 用于控制是否删除拆分結果中的空字元串,通過 splitter#omitemptystrings 設定,trimmer 用于描述删除拆分結果的前後空白符的政策,通過 splitter#trimresults 設定,limit 用于控制拆分的結果個數,通過 splitter#limit 設定。

splitter 支援根據字元、字元串、正則、長度還有 guava 自己的字元比對器 charmatcher 來拆分字元串,基本上每種比對模式的查找方法都不太一樣,但是字元拆分的基本架構又是不變的,政策模式正好合用。

政策接口的定義很簡單,就是傳入一個 splitter 和一個待拆分的字元串,傳回一個疊代器。

然後在重載入參為 charmatcher 的 splitter#on 的時候,傳入一個覆寫了 strategy#iterator 方法的政策執行個體,傳回值是 splittingiterator 這個專用的疊代器。然後 splittingiterator 是個抽象類,需要覆寫實作 separatorstart 和 separatorend 兩個方法才能執行個體化。這兩個方法是 splittingiterator 用到的模闆模式的重要組成。

閱讀源碼的過程在,一個神奇的 continue 的用法讓我震驚了,趕緊 google 一番之後發現這種用法一直都有,隻是我不知道而已。這段代碼出自 splitter#on 的字元串重載。

這裡的 continue 可以直接跳出内循環,然後繼續執行與 positions 标簽平級的循環。如果是 break,就會直接跳出 positions 标簽平級的循環。以前用 c 的時候在跳出多重循環的時候都是用 goto 的,沒想到 java 也提供了類似的功能。

這段 for 循環如果我來實作,估計會寫成這樣,雖然功能差不多,大家的内循環都不緊湊,但是明顯沒有 guava 的實作那麼高貴冷豔,而且我的代碼的計算量要大一些。

惰性求值是函數式程式設計中的常見概念,它的目的是要最小化計算機要做的工作,即把計算推遲到不得不算的時候進行。java 雖然沒有原生支援惰性計算,但是我們依然可以通過一些手段享受惰性計算的好處。

guava 中的疊代器使用了惰性計算的技巧,它不是一開始就算好結果放在清單或集合中,而是在調用 hasnext 方法判斷疊代是否結束時才去計算下一個元素。為了看懂 guava 的惰性疊代器實作,我們要從 abstractiterator 開始。

abstractiterator 使用一個私有的枚舉變量 state 來記錄目前的疊代進度,比如是否找到了下一個元素,疊代是否結束等等。

abstractiterator 給出了一個抽象方法 computenext,計算下一個元素。由于 state 是私有變量,而疊代是否結束隻有在調用 computenext 的過程中才知道,于是我們有了一個保護的 endofdata 方法,允許 abstractiterator 的子類将 state 設定為 state#done。

abstractiterator 實作了疊代器最重要的兩個方法,hasnext 和 next。

hasnext 很容易了解,一上來先判斷疊代器目前狀态,如果已經結束,就傳回 false;如果已經找到下一個元素,就傳回true,不然就試着找找下一個元素。

next 則是先判斷是否還有下一個元素,屬于防禦式程式設計,先對自己做保護;然後把狀态複原到還沒找到下一個元素,然後傳回結果。至于為什麼先把 next 指派給 result,然後把 next 置為 null,最後才傳回 result,我想這可能是個面向 gc 的優化,減少無意義的對象引用。

trytocomputenext 可以認為是對模闆方法 computenext 的包裝調用,首先把狀态置為失敗,然後才調用 computenext。這樣一來,如果計算下一個元素的過程中發生 rte,整個疊代器的狀态就是 state#failed,一旦收到任何調用都會抛出異常。

abstractiterator 的代碼就這些,我們現在知道了它的子類需要覆寫實作 computenext 方法,然後在疊代結束時調用 endofdata。接下來看看 splittingiterator 的實作。

splittingiterator 還是一個抽象類,雖然實作了 computenext 方法,但是它又定義了兩個虛函數 separatorstart 和 separatorend,分别傳回分隔符在指定下标之後第一次出現的下标,和指定下标後面第一個不包含分隔符的下标。之前的政策模式中我們可以看到,這兩個函數在不同的政策中有各自不同的覆寫實作,在 splittingiterator 中,這兩個函數就是模闆函數。

接下來我們看看 splittingiterator 的核心函數 computenext,注意這個函數一直在維護的兩個内部全局變量,offset 和 limit。

進入 while 循環之後,先找找 offset 之後第一個分隔符出現的位置,if 分支處理沒找到的情況,else 分支處理找到了的情況。然後下一個 if 處理的是第一個字元就是分隔符的特殊情況。然後接下來的兩個 while 就開始根據 trimmer 來對找到的元素做前後處理,比如去除空白符之類的。再然後就是根據需要去除那些是空字元串的元素,trim完之後變成空字元串的也會被去除。最後一步操作就是判斷 limit,如果還沒到 limit 的極限,就讓 limit 自減,否則就要調整 end 指針的位置标記 offset 為 -1 然後重新 trim 一下。下一次再調用 computenext 的時候就發現 offset 已經是 -1 了,然後就傳回 endofdata 表示疊代結束。

整個 splitter 最有意思的部分基本上就是這些了,至于 split 函數,其實就是用匿名類函數覆寫技巧調用了一下政策模式中被花樣覆寫實作了的 strategy#iterator 而已。

按理說執行個體化 iterable 接口隻需要實作 iterator 函數即可,這裡覆寫了 tostring 想必是為了友善列印吧?

mapsplitter 的實作中規中矩,使用 outersplitter 拆分鍵值對,使用 entrysplitter 拆分鍵和值,拆分鍵和值前中後各種校驗,然後傳回一個不可修改的 map。

最後說一下 splitter 中一個略顯畫蛇添足的 api,splitter#splittolist。

這個函數其實就是吭哧吭哧把惰性疊代器跑了一遍生成完整資料存放到 arraylist 中,然後又用 collections 把這個清單變成不可修改清單傳回出去,一點都不酷。