Guava 是個風火輪之基礎工具(1)
前言
Guava 是 Java 開發者的好朋友。雖然我在開發中使用 Guava 很長時間了,Guava API 的身影遍及我寫的生產代碼的每個角落,但是我用到的功能隻是 Guava 的功能集中一個少的可憐的真子集,更別說我一直沒有時間認真的去挖掘 Guava 的功能,沒有時間去學習 Guava 的實現。直到最近,我開始閱讀 Getting Started with Google 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 之前就設置空指針保護,是等價的,用戶無需去關心順序問題。
最後更新:2017-05-23 10:02:35