Guava 是個風火輪之基礎工具(1)
前言
Guava 是 Java 開發者的好朋友。雖然我在開發中使用 Guava 很長時間了,Guava API 的身影遍及我寫的生產程式碼的每個角落,但是我用到的功能只是 Guava 的功能集中一個少的可憐的真子集,更別說我一直沒有時間認真的去挖掘 Guava 的功能,沒有時間去學習 Guava 的實現。直到最近,我開始閱讀 ,感覺有必要將我學習和使用 Guava 的一些東西記錄下來。
Joiner
我們經常需要將幾個字串,或者字串陣列、列表之類的東西,拼接成一個以指定符號分隔各個元素的字串,比如把 [1, 2, 3] 拼接成 “1 2 3″。
在 Python 中我只需要簡單的呼叫 str.join 函式,就可以了,就像這樣。
' '.join(map(str, [1, 2, 3]))
到了 Java 中,如果你不知道 Guava 的存在,基本上就得手寫迴圈去實現這個功能,程式碼瞬間變得醜陋起來。
Guava 為我們提供了一套優雅的 API,讓我們能夠輕而易舉的完成字串拼接這一簡單任務。還是上面的例子,藉助 Guava 的 Joiner 類,程式碼瞬間變得優雅起來。
Joiner.on(' ').join(1, 2, 3);
被拼接的物件集,可以是硬編碼的少數幾個物件,可以是實現了 Iterable 介面的集合,也可以是迭代器物件。
除了返回一個拼接過的字串,Joiner 還可以在實現了 Appendable 介面的物件所維護的內容的末尾,追加字串拼接的結果。
StringBuilder sb = new StringBuilder("result:");
Joiner.on(" ").appendTo(sb, 1, 2, 3);
System.out.println(sb);//result:1 2 3
Guava 對空指標有著嚴格的限制,如果傳入的物件中包含空指標,Joiner 會直接丟擲 NPE。與此同時,Joiner 提供了兩個方法,讓我們能夠優雅的處理待拼接集合中的空指標。
如果我們希望忽略空指標,那麼可以呼叫 skipNulls 方法,得到一個會跳過空指標的 Joiner 例項。如果希望將空指標變為某個指定的值,那麼可以呼叫 useForNull 方法,指定用來替換空指標的字串。
Joiner.on(' ').skipNulls().join(1, null, 3);//1 3
Joiner.on(' ').useForNull("None").join(1, null, 3);//1 None 3
需要注意的是,Joiner 例項是不可變的,skipNulls 和 useForNull 都不是在原例項上修改某個成員變數,而是生成一個新的 Joiner 例項。
Joiner.MapJoiner
MapJoiner 是 Joiner 的內部靜態類,用於幫助將 Map 物件拼接成字串。
Joiner.on("#").withKeyValueSeparator("=").join(ImmutableMap.of(1, 2, 3, 4));//1=2#3=4
withKeyValueSeparator 方法指定了鍵與值的分隔符,同時返回一個 MapJoiner 例項。有些傢伙會往 Map 裡插入鍵或值為空指標的鍵值對,如果我們要拼接這種 Map,千萬記得要用 useForNull 對 MapJoiner 做保護,不然 NPE 妥妥的。
原始碼分析
原始碼來自 Guava 18.0。Joiner 類的原始碼約 450 行,其中大部分是註釋、函式過載,常用手法是先實現一個包含完整功能的函式,然後通過各種封裝,把不常用的功能隱藏起來,提供優雅簡介的介面。這樣子的好處顯而易見,使用者可以使用簡單介面解決 80% 的問題,那些罕見而複雜的需求,交給全功能函式去支援。
初始化方法
由於建構函式被設定成了私有,Joiner 只能通過 Joiner#on 函式來初始化。最基礎的 Joiner#on 接受一個字串入參作為分隔符,而接受字元入參的 Joiner#on 方法是前者的過載,內部使用 String#valueOf 函式將字元變成字串後呼叫前者完成初始化。或許這是一個利於字串記憶體回收的優化。
追加拼接結果
整個 Joiner 類最核心的函式莫過於 <A extends Appendable> Joiner#appendTo(A, Iterator<?>)
,一切的字串拼接操作,最後都會呼叫到這個函式。這就是所謂的全功能函式,其他的一切 appendTo 只不過是它的過載,一切的 join 不過是它和它的過載的封裝。
public <A extends Appendable> A appendTo(A appendable, Iterator<?> parts) throws IOException {
checkNotNull(appendable);
if (parts.hasNext()) {
appendable.append(toString(parts.next()));
while (parts.hasNext()) {
appendable.append(separator);
appendable.append(toString(parts.next()));
}
}
return appendable;
}
這段程式碼的第一個技巧是使用 if 和 while 來實現了比較優雅的分隔符拼接,避免了在末尾插入分隔符的尷尬;第二個技巧是使用了自定義的 toString 方法而不是 Object#toString 來將物件序列化成字串,為後續的各種空指標保護開了方便之門。
注意到一個比較有意思的 appendTo 過載。
public final StringBuilder appendTo(StringBuilder builder, Iterator<?> parts) {
try {
appendTo((Appendable) builder, parts);
} catch (IOException impossible) {
throw new AssertionError(impossible);
}
return builder;
}
在 Appendable 介面中,append 方法是會丟擲 IOException 的。然而 StringBuilder 雖然實現了 Appendable,但是它覆蓋實現的 append 方法卻是不丟擲 IOException 的。於是就出現了明知不可能拋異常,卻又不得不去捕獲異常的尷尬。
這裡的異常處理手法十分機智,異常變數命名為 impossible,我們一看就明白這裡是不會丟擲 IOException 的。但是如果 catch 塊裡面什麼都不做又好像不合適,於是丟擲一個 AssertionError,表示對於這裡不拋異常的斷言失敗了。
另一個比較有意思的 appendTo 過載是關於可變長引數。
public final <A extends Appendable> A appendTo(
A appendable, @Nullable Object first, @Nullable Object second, Object... rest)
throws IOException {
return appendTo(appendable, iterable(first, second, rest));
}
注意到這裡的 iterable 方法,它把兩個變數和一個數組變成了一個實現了 Iterable 介面的集合,手法精妙!
private static Iterable<Object> iterable(
final Object first, final Object second, final Object[] rest) {
checkNotNull(rest);
return new AbstractList<Object>() {
@Override public int size() {
return rest.length + 2;
}
@Override public Object get(int index) {
switch (index) {
case 0:
return first;
case 1:
return second;
default:
return rest[index - 2];
}
}
};
}
如果是我來實現,可能是簡單粗暴的建立一個 ArrayList 的例項,然後把這兩個變數一個數組的全部元素放到 ArrayList 裡面然後返回。這樣子程式碼雖然短了,但是代價卻不小:為了一個小小的過載呼叫而產生了 O(n) 的時空複雜度。
看看人家 G 社的做法。要想寫出這樣的程式碼,需要熟悉順序表迭代器的實現。迭代器內部維護著一個遊標,cursor。迭代器的兩大關鍵操作,hasNext 判斷是否還有沒遍歷的元素,next 獲取下一個元素,它們的實現是這樣的。
public boolean hasNext() {
return cursor != size();
}
public E next() {
checkForComodification();
try {
int i = cursor;
E next = get(i);
lastRet = i;
cursor = i + 1;
return next;
} catch (IndexOutOfBoundsException e) {
checkForComodification();
throw new NoSuchElementException();
}
}
hasNext 中關鍵的函式呼叫是 size,獲取集合的大小。next 方法中關鍵的函式呼叫是 get,獲取第 i 個元素。Guava 的實現返回了一個被覆蓋了 size 和 get 方法的 AbstractList,巧妙的複用了由編譯器生成的陣列,避免了新建列表和增加元素的開銷。
空指標處理
當待拼接列表中可能包含空指標時,我們用 useForNull 將空指標替換為我們指定的字串。它是通過返回一個覆蓋了方法的 Joiner 例項來實現的。
public Joiner useForNull(final String nullText) {
checkNotNull(nullText);
return new Joiner(this) {
@Override CharSequence toString(@Nullable Object part) {
return (part == null) ? nullText : Joiner.this.toString(part);
}
@Override public Joiner useForNull(String nullText) {
throw new UnsupportedOperationException("already specified useForNull");
}
@Override public Joiner skipNulls() {
throw new UnsupportedOperationException("already specified useForNull");
}
};
}
首先是使用複製建構函式保留先前初始化時候設定的分隔符,然後覆蓋了之前提到的 toString 方法。為了防止重複呼叫 useForNull 和 skipNulls,還特意覆蓋了這兩個方法,一旦呼叫就丟擲執行時異常。為什麼不能重複呼叫 useForNull ?因為覆蓋了 toString 方法,而覆蓋實現中需要呼叫覆蓋前的 toString。
在不支援的操作中丟擲 UnsupportedOperationException 是 Guava 的常見做法,可以在第一時間糾正不科學的呼叫方式。
skipNulls 的實現就相對要複雜一些,覆蓋了原先全功能 appendTo 中使用 if 和 while 的優雅實現,變成了 2 個 while 先後執行。第一個 while 找到 第一個不為空指標的元素,起到之前的 if 的功能,第二個 while 功能和之前的一致。
public Joiner skipNulls() {
return new Joiner(this) {
@Override public <A extends Appendable> A appendTo(A appendable, Iterator<?> parts)
throws IOException {
checkNotNull(appendable, "appendable");
checkNotNull(parts, "parts");
while (parts.hasNext()) {
Object part = parts.next();
if (part != null) {
appendable.append(Joiner.this.toString(part));
break;
}
}
while (parts.hasNext()) {
Object part = parts.next();
if (part != null) {
appendable.append(separator);
appendable.append(Joiner.this.toString(part));
}
}
return appendable;
}
@Override public Joiner useForNull(String nullText) {
throw new UnsupportedOperationException("already specified skipNulls");
}
@Override public MapJoiner withKeyValueSeparator(String kvs) {
throw new UnsupportedOperationException("can't use .skipNulls() with maps");
}
};
}
拼接鍵值對
MapJoiner 實現為 Joiner 的一個靜態內部類,它的建構函式和 Joiner 一樣也是私有,只能通過 Joiner#withKeyValueSeparator 來生成例項。類似地,MapJoiner 也實現了 appendTo 方法和一系列的過載,還用 join 方法對 appendTo 做了封裝。MapJoiner 整個實現和 Joiner 大同小異,在實現中大量 Joiner 的 toString 方法來保證空指標保護行為和初始化時的語義一致。
MapJoiner 也實現了一個 useForNull 方法,這樣的好處是,在獲取 MapJoiner 之後再去設定空指標保護,和獲取 MapJoiner 之前就設定空指標保護,是等價的,使用者無需去關心順序問題。