1. 程式人生 > >Guava 是個風火輪之基礎工具(1)

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