本文是對 Guava 中 Splitter 的學(xué)習(xí)介紹弧可。歡迎加入學(xué)習(xí)項目: LearningGuava蛀柴。
使用示例
以下參考:官方文檔吞滞。
Splitter
概述
Java 中關(guān)于分詞的工具類會有一些古怪的行為掰读。例如:String.split
函數(shù)會悄悄地丟棄尾部分割符亡容,而 StringTokenizer
處理5個空格字符串品洛,結(jié)果將會什么都沒有树姨。
問題:",a,,b,".split(",")
的結(jié)果是什么?
- "", "a", "", "b", ""
-
null
, "a",null
, "b",null
- "a",
null
, "b" - "a", "b"
- 以上都不是
正確答案是:5 以上都不是桥状,應(yīng)該是 "", "a", "", "b"
帽揪。只有尾隨的空字符串被跳過。這樣的結(jié)果很令人費解辅斟。
Splitter 可以讓你使用一種非常簡單流暢的模式來控制這些令人困惑的行為转晰。
Splitter.on(',')
.trimResults()
.omitEmptyStrings()
.split("foo,bar,, qux");
以上代碼將會返回 Iterable<String>
,包含 "foo"、 "bar"查邢、 "qux"蔗崎。一個 Splitter
可以通過這些來進(jìn)行劃分:Pattern
、char
扰藕、 String
缓苛、CharMatcher
。
如果你希望返回的是 List
的話邓深,可以使用這樣的代碼 Lists.newArrayList(splitter.split(string))
未桥。
工廠函數(shù)
方法 | 描述 | 例子 |
---|---|---|
Splitter.on(char) |
基于特定字符劃分 | Splitter.on(';') |
Splitter.on(CharMatcher) |
基于某些類別劃分 | Splitter.on(';') |
Splitter.on(String) |
基于字符串劃分 |
Splitter.on(CharMatcher.BREAKING_WHITESPACE) Splitter.on(CharMatcher.anyOf(";,."))
|
Splitter.on(Pattern) Splitter.onPattern(String)
|
基于正則表達(dá)式劃分 | Splitter.on(", ") |
Splitter.fixedLength(int) |
按指定長度劃分,最后部分可以小于指定長度但不能為空 | Splitter.fixedLength(3) |
修改器
方法 | 描述 | 例子 |
---|---|---|
omitEmptyStrings() |
移去結(jié)果中的空字符串 |
Splitter.on(',').omitEmptyStrings().split("a,,c,d") 返回 "a", "c", "d"
|
trimResults() |
將結(jié)果中的空格刪除芥备,等價于trimResults(CharMatcher.WHITESPACE)
|
Splitter.on(',').trimResults().split("a, b, c, d") 返回 "a", "b", "c", "d"
|
trimResults(CharMatcher) |
移除匹配字符 |
Splitter.on(',').trimResults(CharMatcher.is('_')).split("_a ,_b_ ,c__") 返回 "a ", "b_ ", "c"
|
limit(int) |
達(dá)到指定數(shù)目后停止字符串的劃分 |
Splitter.on(',').limit(3).split("a,b,c,d") 返回 "a", "b", "c,d"
|
Splitter.MapSplitter
以下參考:Guava 是個風(fēng)火輪之基礎(chǔ)工具(2)冬耿。
通過 Splitter
的 withKeyValueSeparator
方法可以獲得 Joiner.MapJoiner
對象。
MapSpliter
只有一個公共方法萌壳,如下所示淆党。可以看到返回的對象是 Map<String, String>
讶凉。
public Map<String, String> split(CharSequence sequence)
以下代碼將返回這樣的 Map
: {"1":"2", "3":"4"}
染乌。
Splitter.on("#").withKeyValueSeparator(":").split("1:2#3:4");
需要注意的是,MapSplitter
對鍵值對格式有著嚴(yán)格的校驗懂讯,下例會拋出 java.lang.IllegalArgumentException
異常荷憋。
Splitter.on("#").withKeyValueSeparator(":").split("1:2#3:4:5");
因此,如果希望使用 MapSplitter
來拆分 KV 結(jié)構(gòu)的字符串褐望,需要保證鍵-值分隔符和鍵值對之間的分隔符不會稱為鍵或值的一部分勒庄。也許是出于類似方面的考慮,MapSplitter 被加上了 @Beta
注解(未來不保證兼容瘫里,甚至可能會移除)实蔽。所以一般推薦使用 JSON
而不是 MapJoiner
+ MapSplitter
。
源碼分析
以下參考:Guava 是個風(fēng)火輪之基礎(chǔ)工具(2)谨读。
Splitter
的實現(xiàn)中有十分明顯的策略模式和模板模式局装,有各種神乎其技的方法覆蓋,還有 Guava 久負(fù)盛名的迭代技巧和惰性計算劳殖。
成員變量
Splitter
類有 4 個成員變量:
-
CharMatcher trimmer
:用于描述刪除拆分結(jié)果的前后指定字符的策略铐尚。 -
boolean omitEmptyStrings
:用于控制是否刪除拆分結(jié)果中的空字符串。 -
Strategy strategy
:用于幫助實現(xiàn)策略模式哆姻。 -
int limit
:用于控制拆分的結(jié)果個數(shù)宣增。
策略模式
Splitter
可以根據(jù)字符、字符串矛缨、正則爹脾、長度還有 Guava 自己的字符匹配器 CharMatcher
來拆分字符串帖旨,基本上每種匹配模式的查找方法都不太一樣,但是字符拆分的基本框架又是不變的灵妨,所以策略模式正好合用碉就。
策略接口的定義很簡單,就是傳入一個 Splitter
和一個待拆分的字符串闷串,返回一個迭代器瓮钥。
private interface Strategy {
Iterator<String> iterator(Splitter splitter, CharSequence toSplit);
}
每個工廠函數(shù)創(chuàng)建最后都需要去調(diào)用基本的私有構(gòu)造函數(shù)。這個創(chuàng)建過程中烹吵,主要是提供一個可以創(chuàng)建 Iterator<String>
的 Strategy
碉熄。
private Splitter(Strategy strategy, boolean omitEmptyStrings, CharMatcher trimmer, int limit);
以 Splitter on(final CharMatcher separatorMatcher)
創(chuàng)建函數(shù)為例肋拔,這里返回的是 SplittingIterator
(它是個抽象類锈津,繼承了 AbstractIterator
,而 AbstractIterator
繼承了 Iterator
)凉蜂。
public static Splitter on(final CharMatcher separatorMatcher) {
checkNotNull(separatorMatcher);
return new Splitter(
new Strategy() {
@Override
public SplittingIterator iterator(Splitter splitter, final CharSequence toSplit) {
return new SplittingIterator(splitter, toSplit) {
@Override
int separatorStart(int start) {
return separatorMatcher.indexIn(toSplit, start);
}
@Override
int separatorEnd(int separatorPosition) {
return separatorPosition + 1;
}
};
}
});
}
SplittingIterator
需要覆蓋實現(xiàn) separatorStart
和 separatorEnd
兩個方法才能實例化琼梆。這兩個方法也是 SplittingIterator
用到的模板模式的重要組成。
惰性迭代器與模板模式
惰性計算目的是要最小化計算機(jī)要做的工作窿吩,即把計算推遲到不得不算的時候進(jìn)行茎杂。Java中的惰性計算可以參考《你應(yīng)該更新的 Java 知識之惰性求值:Supplier 和 Guava》。
Guava 中的迭代器使用了惰性計算的技巧纫雁,它不是一開始就算好結(jié)果放在列表或集合中煌往,而是在調(diào)用 hasNext
方法判斷迭代是否結(jié)束時才去計算下一個元素。
AbstractIterator
為了看懂 Guava 的惰性迭代器實現(xiàn)轧邪,我們要從 AbstractIterator
開始刽脖。
AbstractIterator
使用私有的枚舉變量 state
來記錄當(dāng)前的迭代進(jìn)度,比如是否找到了下一個元素忌愚,迭代是否結(jié)束等曲管。AbstractIterator
有一個抽象方法 computeNext
,負(fù)責(zé)計算下一個元素硕糊。由于 state
是私有變量院水,而迭代是否結(jié)束只有在調(diào)用 computeNext
的過程中才知道,于是提供了一個保護(hù)的 endOfData
方法癌幕,允許子類將 state
設(shè)置為 State.DONE
衙耕。
private enum State {
READY,
NOT_READY,
DONE,
FAILED,
}
AbstractIterator
實現(xiàn)了迭代器最重要的兩個方法,hasNext
和 next
勺远。
hasNext
很容易理解,一上來先判斷迭代器當(dāng)前狀態(tài)时鸵,如果已經(jīng)結(jié)束胶逢,就返回 false
厅瞎;如果已經(jīng)找到下一個元素,就返回 true
初坠,不然就試著找找下一個元素和簸。
@Override
public final boolean hasNext() {
checkState(state != State.FAILED);
switch (state) {
case READY:
return true;
case DONE:
return false;
default:
}
return tryToComputeNext();
}
next
則是先判斷是否還有下一個元素,屬于防御式編程碟刺,先對自己做保護(hù)锁保;然后把狀態(tài)復(fù)原到還沒找到下一個元素,然后返回結(jié)果半沽。至于為什么要把 next
置為 null
爽柒,可能是幫助 JVM 回收對象。
@Override
public final T next() {
if (!hasNext()) {
throw new NoSuchElementException();
}
state = State.NOT_READY;
T result = next;
next = null;
return result;
}
tryToComputeNext
可以認(rèn)為是對模板方法 computeNext
的包裝調(diào)用者填,首先把狀態(tài)置為失敗浩村,然后才調(diào)用 computeNext。這樣一來占哟,如果計算下一個元素的過程中發(fā)生 RuntimeException
心墅,整個迭代器的狀態(tài)就是 State.FAILED
,一旦收到任何調(diào)用都會拋出異常榨乎。
private boolean tryToComputeNext() {
state = State.FAILED; // 暫時悲觀
next = computeNext();
if (state != State.DONE) {
state = State.READY;
return true;
}
return false;
}
AbstractIterator
的代碼就這些怎燥,我們現(xiàn)在知道了它的子類需要覆蓋實現(xiàn) computeNext
方法,然后在迭代結(jié)束時調(diào)用 endOfData
蜜暑。接下來看看 SplittingIterator
的實現(xiàn)刺覆。
SplittingIterator
SplittingIterator
還是一個抽象類,雖然實現(xiàn)了 computeNext
方法史煎,但是它又定義了兩個虛函數(shù):
-
separatorStart
: 返回分隔符在指定下標(biāo)之后第一次出現(xiàn)的下標(biāo) -
separatorEnd
: 返回分隔符在指定下標(biāo)后面第一個不包含分隔符的下標(biāo)谦屑。
之前的策略模式中我們可以看到,這兩個函數(shù)在不同的策略中有各自不同的覆蓋實現(xiàn)篇梭,在 SplittingIterator
中氢橙,這兩個函數(shù)就是模板函數(shù)。
接下來看看 SplittingIterator
的核心函數(shù) computeNext
恬偷,這個函數(shù)一直在維護(hù)的兩個內(nèi)部全局變量: offset
和 limit
悍手。
@Override
protected String computeNext() {
// 返回的字符串介于上一個分隔符和下一個分隔符之間。
// nextStart 是返回子串的起始位置袍患,offset 是下次開啟尋找分隔符的地方坦康。
int nextStart = offset;
while (offset != -1) {
int start = nextStart;
int end;
// 找 offset 之后第一個分隔符出現(xiàn)的位置
int separatorPosition = separatorStart(offset);
if (separatorPosition == -1) {
// 處理沒找到的情況
end = toSplit.length();
offset = -1;
} else {
// 處理找到的情況
end = separatorPosition;
offset = separatorEnd(separatorPosition);
}
// 處理的是第一個字符就是分隔符的特殊情況
if (offset == nextStart) {
// 發(fā)生情況:空字符串 或者 整個字符串都沒有匹配。
// offset 需要增加來尋找這個位置之后的分隔符诡延,
// 但是沒有改變接下來返回字符串的 start 的位置滞欠,
// 所以此時它們二者相同。
offset++;
if (offset > toSplit.length()) {
offset = -1;
}
continue;
}
// 根據(jù) trimmer 來對找到的元素做前處理肆良,比如去除空白符之類的筛璧。
while (start < end && trimmer.matches(toSplit.charAt(start))) {
start++;
}
// 根據(jù) trimmer 來對找到的元素做后處理逸绎,比如去除空白符之類的。
while (end > start && trimmer.matches(toSplit.charAt(end - 1))) {
end--;
}
// 根據(jù)需要去除那些是空字符串的元素夭谤,trim完之后變成空字符串的也會被去除棺牧。
if (omitEmptyStrings && start == end) {
// Don't include the (unused) separator in next split string.
nextStart = offset;
continue;
}
// 判斷 limit,
if (limit == 1) {
// The limit has been reached, return the rest of the string as the
// final item. This is tested after empty string removal so that
// empty Strings do not count towards the limit.
end = toSplit.length();
// 調(diào)整 end 指針的位置標(biāo)記 offset 為 -1朗儒,下一次再調(diào)用 computeNext
// 的時候就發(fā)現(xiàn) offset 已經(jīng)是 -1 了颊乘,然后就返回 endOfData 表示迭代結(jié)束。
offset = -1;
// Since we may have changed the end, we need to trim it again.
while (end > start && trimmer.matches(toSplit.charAt(end - 1))) {
end--;
}
} else {
// 還沒到 limit 的極限醉锄,就讓 limit 自減
limit--;
}
return toSplit.subSequence(start, end).toString();
}
return endOfData();
}
}