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

Guava 是個風火輪之基礎工具(2)

前言

Guava 是 Java 開發者的好朋友。雖然我在開發中使用 Guava 很長時間了,Guava API 的身影遍及我寫的生產程式碼的每個角落,但是我用到的功能只是 Guava 的功能集中一個少的可憐的真子集,更別說我一直沒有時間認真的去挖掘 Guava 的功能,沒有時間去學習 Guava 的實現。直到最近,我開始閱讀 ,感覺有必要將我學習和使用 Guava 的一些東西記錄下來。

Splitter

Guava 提供了 Joiner 類用於將多個物件拼接成字串,如果我們需要一個反向的操作,就要用到 Splitter 類。Splitter 能夠將一個字串按照指定的分隔符拆分成可迭代遍歷的字串集合,Iterable<String>

Splitter 的 API 和 Joiner 類似,使用 Splitter#on 指定分隔符,使用 Splitter#split 完成拆分。

Splitter.on(' ').split("1 2 3");//["1", "2", "3"]

Splitter 還支援使用正則表示式來描述分隔符。

Splitter.onPattern("\\s+").split("1 \t   2 3");//["1", "2", "3"]

Splitter 還支援根據長度來拆分字串。

Splitter.fixedLength(3).split("1 2 3");//["1 2", " 3"]

Splitter.MapSplitter

與 Joiner.MapJoiner 相對,Splitter.MapSplitter 用來拆分被拼接了的 Map 物件,返回 Map<String, String>

Splitter.on("#").withKeyValueSeparator(":").split("1:2#3:4");//{"1":"2", "3":"4"}

需要注意的是,不是所有由 MapJoiner 拼接出來的字串,都能夠被 MapSplitter 拆分,MapSplitter 對鍵值對個格式有著嚴格的校驗。比如下面的拆分會丟擲異常。

Splitter.on("#").withKeyValueSeparator(":"
).split("1:2#3:4:5"); //java.lang.IllegalArgumentException: Chunk [3:4:5] is not a valid entry

因此,如果希望使用 MapSplitter 來拆分 KV 結構的字串,需要保證鍵-值分隔符和鍵值對之間的分隔符不會稱為鍵或值的一部分。也許是出於類似方面的考慮,MapSplitter 被加上了 @Beta 註解,也許在不久的將來它會被移除,或者有大的變化。如果在應用中有可能用到 KV 結構的字串,我一般推薦使用 JSON 而不是 MapJoiner + MapSplitter。

原始碼分析

原始碼來自 Guava 18.0。Splitter 類原始碼約 600 行,依舊大部分是註釋和函式過載。Splitter 的實現中有十分明顯的策略模式和模板模式,有各種神乎其技的方法覆蓋,還有 Guava 久負盛名的迭代技巧和惰性計算。

不得不說,平時翻閱一些基礎類庫,總是感覺 “這種程式碼我也能寫”,“這程式碼寫的還沒我好”,“在工具類中強依賴日誌元件,人幹事?”,如果 IDE 配上彈幕恐怕全是吐槽,難有讓人精神為之一振的程式碼。閱讀 Guava 的程式碼,每次都有新的驚喜,各種神技巧黑科技讓我五體投地,寫程式碼的腦洞半徑屢次被 Guava 撐大。

成員變數

Splitter 類有 4 個成員變數,strategy 用於幫助實現策略模式,omitEmptyStrings 用於控制是否刪除拆分結果中的空字串,通過 Splitter#omitEmptyStrings 設定,trimmer 用於描述刪除拆分結果的前後空白符的策略,通過 Splitter#trimResults 設定,limit 用於控制拆分的結果個數,通過 Splitter#limit 設定。

策略模式

Splitter 支援根據字元、字串、正則、長度還有 Guava 自己的字元匹配器 CharMatcher 來拆分字串,基本上每種匹配模式的查詢方法都不太一樣,但是字元拆分的基本框架又是不變的,策略模式正好合用。

策略介面的定義很簡單,就是傳入一個 Splitter 和一個待拆分的字串,返回一個迭代器。

private interface Strategy {
  Iterator<String> iterator(Splitter splitter, CharSequence toSplit);
}

然後在過載入參為 CharMatcher 的 Splitter#on 的時候,傳入一個覆蓋了 Strategy#iterator 方法的策略例項,返回值是 SplittingIterator 這個專用的迭代器。然後 SplittingIterator 是個抽象類,需要覆蓋實現 separatorStart 和 separatorEnd 兩個方法才能例項化。這兩個方法是 SplittingIterator 用到的模板模式的重要組成。

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;
        }
      };
    }
  });
}

閱讀原始碼的過程在,一個神奇的 continue 的用法讓我震驚了,趕緊 Google 一番之後發現這種用法一直都有,只是我不知道而已。這段程式碼出自 Splitter#on 的字串過載。

return new SplittingIterator(splitter, toSplit) {
  @Override public int separatorStart(int start) {
    int separatorLength = separator.length();
    positions:
    for (int p = start, last = toSplit.length() - separatorLength; p <= last; p++) {
      for (int i = 0; i < separatorLength; i++) {
        if (toSplit.charAt(i + p) != separator.charAt(i)) {
          continue positions;
        }
      }
      return p;
    }
    return -1;
  }
  @Override public int separatorEnd(int separatorPosition) {
    return separatorPosition + separator.length();
  }
};

這裡的 continue 可以直接跳出內迴圈,然後繼續執行與 positions 標籤平級的迴圈。如果是 break,就會直接跳出 positions 標籤平級的迴圈。以前用 C 的時候在跳出多重迴圈的時候都是用 goto 的,沒想到 Java 也提供了類似的功能。

這段 for 迴圈如果我來實現,估計會寫成這樣,雖然功能差不多,大家的內迴圈都不緊湊,但是明顯沒有 Guava 的實現那麼高貴冷豔,而且我的程式碼的計算量要大一些。

for (int p = start, last = toSplit.length() - separatorLength; p <= last; p++) {
  boolean match = true;
  for (int i = 0; i < separatorLength; i++) {
    match &= (toSplit.charAt(i + p) == separator.charAt(i))
  }
  if (match) {
    return p;
  }
}

惰性迭代器與模板模式

惰性求值是函數語言程式設計中的常見概念,它的目的是要最小化計算機要做的工作,即把計算推遲到不得不算的時候進行。Java 雖然沒有原生支援惰性計算,但是我們依然可以通過一些手段享受惰性計算的好處。

Guava 中的迭代器使用了惰性計算的技巧,它不是一開始就算好結果放在列表或集合中,而是在呼叫 hasNext 方法判斷迭代是否結束時才去計算下一個元素。為了看懂 Guava 的惰性迭代器實現,我們要從 AbstractIterator 開始。

AbstractIterator 使用一個私有的列舉變數 state 來記錄當前的迭代進度,比如是否找到了下一個元素,迭代是否結束等等。

private enum State {
  READY, NOT_READY, DONE, FAILED,
}

AbstractIterator 給出了一個抽象方法 computeNext,計算下一個元素。由於 state 是私有變數,而迭代是否結束只有在呼叫 computeNext 的過程中才知道,於是我們有了一個保護的 endOfData 方法,允許 AbstractIterator 的子類將 state 設定為 State#DONE。

AbstractIterator 實現了迭代器最重要的兩個方法,hasNext 和 next。

@Override
public final boolean hasNext() {
  checkState(state != State.FAILED);
  switch (state) {
    case DONE:
      return false;
    case READY:
      return true;
    default:
  }
  return tryToComputeNext();
}

@Override
public final T next() {
  if (!hasNext()) {
    throw new NoSuchElementException();
  }
  state = State.NOT_READY;
  T result = next;
  next = null;
  return result;
}

hasNext 很容易理解,一上來先判斷迭代器當前狀態,如果已經結束,就返回 false;如果已經找到下一個元素,就返回true,不然就試著找找下一個元素。

next 則是先判斷是否還有下一個元素,屬於防禦式程式設計,先對自己做保護;然後把狀態復原到還沒找到下一個元素,然後返回結果。至於為什麼先把 next 賦值給 result,然後把 next 置為 null,最後才返回 result,我想這可能是個面向 GC 的優化,減少無意義的物件引用。

private boolean tryToComputeNext() {
  state = State.FAILED; // temporary pessimism
  next = computeNext();
  if (state != State.DONE) {
    state = State.READY;
    return true;
  }
  return false;
}

tryToComputeNext 可以認為是對模板方法 computeNext 的包裝呼叫,首先把狀態置為失敗,然後才呼叫 computeNext。這樣一來,如果計算下一個元素的過程中發生 RTE,整個迭代器的狀態就是 State#FAILED,一旦收到任何呼叫都會丟擲異常。

AbstractIterator 的程式碼就這些,我們現在知道了它的子類需要覆蓋實現 computeNext 方法,然後在迭代結束時呼叫 endOfData。接下來看看 SplittingIterator 的實現。

SplittingIterator 還是一個抽象類,雖然實現了 computeNext 方法,但是它又定義了兩個虛擬函式 separatorStart 和 separatorEnd,分別返回分隔符在指定下標之後第一次出現的下標,和指定下標後面第一個不包含分隔符的下標。之前的策略模式中我們可以看到,這兩個函式在不同的策略中有各自不同的覆蓋實現,在 SplittingIterator 中,這兩個函式就是模板函式。

接下來我們看看 SplittingIterator 的核心函式 computeNext,注意這個函式一直在維護的兩個內部全域性變數,offset 和 limit。

@Override protected String computeNext() {
  /*
   * The returned string will be from the end of the last match to the
   * beginning of the next one. nextStart is the start position of the
   * returned substring, while offset is the place to start looking for a
   * separator.
   */
  int nextStart = offset;
  while (offset != -1) {
    int start = nextStart;
    int end;

    int separatorPosition = separatorStart(offset);
    if (separatorPosition == -1) {
      end = toSplit.length();
      offset = -1;
    } else {
      end = separatorPosition;
      offset = separatorEnd(separatorPosition);
    }
    if (offset == nextStart) {
      /*
       * This occurs when some pattern has an empty match, even if it
       * doesn't match the empty string -- for example, if it requires
       * lookahead or the like. The offset must be increased to look for
       * separators beyond this point, without changing the start position
       * of the next returned substring -- so nextStart stays the same.
       */
      offset++;
      if (offset >= toSplit.length()) {
        offset = -1;
      }
      continue;
    }
    while (start < end && trimmer.matches(toSplit.charAt(start))) {
      start++;
    }
    while (end > start && trimmer.matches(toSplit.charAt(end - 1))) {
      end--;
    }
    if (omitEmptyStrings && start == end) {
      // Don't include the (unused) separator in next split string.
      nextStart = offset;
      continue;
    }
    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();
      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--;
    }
    return toSplit.subSequence(start, end).toString();
  }
  return endOfData();
}

進入 while 迴圈之後,先找找 offset 之後第一個分隔符出現的位置,if 分支處理沒找到的情況,else 分支處理找到了的情況。然後下一個 if 處理的是第一個字元就是分隔符的特殊情況。然後接下來的兩個 while 就開始根據 trimmer 來對找到的元素做前後處理,比如去除空白符之類的。再然後就是根據需要去除那些是空字串的元素,trim完之後變成空字串的也會被去除。最後一步操作就是判斷 limit,如果還沒到 limit 的極限,就讓 limit 自減,否則就要調整 end 指標的位置標記 offset 為 -1 然後重新 trim 一下。下一次再呼叫 computeNext 的時候就發現 offset 已經是 -1 了,然後就返回 endOfData 表示迭代結束。

整個 Splitter 最有意思的部分基本上就是這些了,至於 split 函式,其實就是用匿名類函式覆蓋技巧呼叫了一下策略模式中被花樣覆蓋實現了的 Strategy#iterator 而已。

public Iterable<String> split(final CharSequence sequence) {
  checkNotNull(sequence);
  return new Iterable<String>() {
    @Override public Iterator<String> iterator() {
      return splittingIterator(sequence);
    }
    @Override public String toString() {
      return Joiner.on(", ")
          .appendTo(new StringBuilder().append('['), this)
          .append(']')
          .toString();
    }
  };
}

按理說例項化 Iterable 介面只需要實現 iterator 函式即可,這裡覆蓋了 toString 想必是為了方便列印吧?

MapSplitter 的實現中規中矩,使用 outerSplitter 拆分鍵值對,使用 entrySplitter 拆分鍵和值,拆分鍵和值前中後各種校驗,然後返回一個不可修改的 Map。

最後說一下 Splitter 中一個略顯畫蛇添足的 API,Splitter#splitToList。

public List<String> splitToList(CharSequence sequence) {
  checkNotNull(sequence);
  Iterator<String> iterator = splittingIterator(sequence);
  List<String> result = new ArrayList<String>();
  while (iterator.hasNext()) {
    result.add(iterator.next());
  }
  return Collections.unmodifiableList(result);
}

這個函式其實就是吭哧吭哧把惰性迭代器跑了一遍生成完整資料存放到 ArrayList 中,然後又用 Collections 把這個列表變成不可修改列表返回出去,一點都不酷。