閱讀304 返回首頁    go 阿裏雲 go 技術社區[雲棲]


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

前言

Guava 是 Java 開發者的好朋友。雖然我在開發中使用 Guava 很長時間了,Guava API 的身影遍及我寫的生產代碼的每個角落,但是我用到的功能隻是 Guava 的功能集中一個少的可憐的真子集,更別說我一直沒有時間認真的去挖掘 Guava 的功能,沒有時間去學習 Guava 的實現。直到最近,我開始閱讀 Getting Started with Google 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 把這個列表變成不可修改列表返回出去,一點都不酷。

最後更新:2017-05-23 09:31:42

  上一篇:go  線程安全及不可變性
  下一篇:go  Using Big Data to Build Customer Loyalty