Joiner


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 之前就設置空指針保護,是等價的,用戶無需去關心順序問題。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM