強大的 Guava 工具類

rickiyang發表於2021-04-15

Java 開發的同學應該都使用或者聽說過 Google 提供的 Guava 工具包。日常使用最多的肯定是集合相關的工具類,還有 Guava cache,除了這些之外 Guava 還提供了很多有用的功能,鑑於日常想用的時候找不到,這裡就梳理一下 Guava 中那些好用的工具類,想優化程式碼的時候不妨過來看看!

集合

普通集合
List<String> list = Lists.newArrayList();
Set<String> set = Sets.newHashSet();
Map<String, String> map = Maps.newHashMap();
Set 取交集、並集、差集
HashSet<Integer> setA = Sets.newHashSet(1, 2, 3, 4, 5);
HashSet<Integer> setB = Sets.newHashSet(4, 5, 6, 7, 8);

Sets.SetView<Integer> union = Sets.union(setA, setB);
System.out.println("union:" + union);

Sets.SetView<Integer> difference = Sets.difference(setA, setB);
System.out.println("difference:" + difference);

Sets.SetView<Integer> intersection = Sets.intersection(setA, setB);
System.out.println("intersection:" + intersection);
map 取交集、並集、差集
HashMap<String, Integer> mapA = Maps.newHashMap();
mapA.put("a", 1);
mapA.put("b", 2);
mapA.put("c", 3);

HashMap<String, Integer> mapB = Maps.newHashMap();
mapB.put("b", 20);
mapB.put("c", 3);
mapB.put("d", 4);

MapDifference<String, Integer> differenceMap = Maps.difference(mapA, mapB);
Map<String, MapDifference.ValueDifference<Integer>> entriesDiffering = differenceMap.entriesDiffering();
//左邊差集
Map<String, Integer> entriesOnlyLeft = differenceMap.entriesOnlyOnLeft();
//右邊差集
Map<String, Integer> entriesOnlyRight = differenceMap.entriesOnlyOnRight();
//交集
Map<String, Integer> entriesInCommon = differenceMap.entriesInCommon();

System.out.println(entriesDiffering);   // {b=(2, 20)}
System.out.println(entriesOnlyLeft);    // {a=1}
System.out.println(entriesOnlyRight);   // {d=4}
System.out.println(entriesInCommon);    // {c=3}
不可變集合(immutable)

不可變集合的特性有:

  • 在多執行緒操作下,是執行緒安全的;
  • 所有不可變集合會比可變集合更有效的利用資源;
  • 中途不可改變。

如果你的需求是想建立一個一經初始化後就不能再被改變的集合那麼它適合你,因為這些工具類根本就沒給你提供修改的 API,這意味著你連犯錯誤的機會都沒有。

ImmutableList<Integer> iList = ImmutableList.of(12,54,87);
ImmutableSet<Integer> iSet = ImmutableSet.of(354,54,764,354);
ImmutableMap<String, Integer> iMap = ImmutableMap.of("k1", 453, "k2", 534);

以上 Immutable 開頭的相關集合類的 add、remove 方法都被宣告為 deprecated。當你手誤點到了這些方法發現是 deprecated 的時候你不會還想著使用吧。

注意:每個Guava immutable集合類的實現都拒絕 null 值。

有趣的集合

MultiSet: 無序+可重複

我們映像中的 Set 應該是無序的,元素不可重複的。MultiSet 顛覆了三觀,因為它可以重複。

定義一個 MultiSet 並新增元素:

Multiset<Integer> set = HashMultiset.create();
set.add(3);
set.add(3);
set.add(4);
set.add(5);
set.add(4);

你還可以新增指定個數的同一個元素:

set.add(7, 3);

這表示你想新增 3 個 7。

列印出來的 MultiSet 也很有意思:

[3 x 2, 4 x 2, 5, 7 x 3]

2 個 3,2 個 4, 一個 5, 3 個 7。

獲取某個元素的個數:

int count = set.count(3);

這個工具類確實很有意思,幫我們實現了 word count。

Multimap :key 可以重複的 map

這個 map 也很有意思。正常的 map 為了區分不同的 key,它倒好,直接給你來一樣的 key 。

Multimap<String, String> map = LinkedHashMultimap.create();
map.put("key", "haha");
map.put("key", "haha1");

Collection<String> key = map.get("key");
System.out.println(key);

使用很簡單,用一個 key 可以獲取到該 key 對應的兩個值,結果用 list 返回。恕我無知,我還沒想到這個 map 能夠使用的場景。

Multimap 提供了多種實現:

Multimap 實現 key 欄位型別 value 欄位型別
ArrayListMultimap HashMap ArrayList
HashMultimap HashMap HashSet
LinkedListMultimap LinkedHashMap LinkedList
LinkedHashMultimap LinkedHashMap LinkedHashSet
TreeMultimap TreeMap TreeSet
ImmutableListMultimap ImmutableMap ImmutableList
ImmutableSetMultimap ImmutableMap ImmutableSet
BiMap:雙向 Map (Bidirectional Map) 鍵與值都不能重複

這個稍稍正常一點。如果 key 重複了則會覆蓋 key ,如果 value 重複了則會報錯。

public static void main(String[] args) {
  BiMap<String, String> biMap = HashBiMap.create();
  biMap.put("key", "haha");
  biMap.put("key", "haha1");
  biMap.put("key1", "haha");

  String value = biMap.get("key");
  System.out.println(value);
}

上面的示例中鍵 ”key“ 有兩個,執行可以發現 get 的時候會用 ”haha1" 覆蓋 ”haha“,另外 value 為 ”haha“ 也有兩個,你會發現執行上面的程式碼不會報錯,這是因為 ”key“ 對應的 value 已經被 "haha1" 覆蓋了。否則是會報錯。

雙鍵 map - 超級實用

雙鍵的 map ,我突然感覺我發現了新大陸。比如我有一個業務場景是:根據職位和部門將公司人員區分開來。key 可以用職位 + 部門組成一個字串,那我們有了雙鍵 map 之後就沒這種煩惱。

public static void main(String[] args) {
  Table<String, String, List<Object>> tables = HashBasedTable.create();
  tables.put("財務部", "總監", Lists.newArrayList());
  tables.put("財務部", "職員",Lists.newArrayList());
  tables.put("法務部", "助理",Lists.newArrayList());
  System.out.println(tables);
}
工具類

JDK裡大家耳熟能詳的是Collections 這個集合工具類, 提供了一些基礎的集合處理轉換功能, 但是實際使用裡很多需求並不是簡單的排序, 或者比較數值大小, 然後 Guava 在此基礎上做了許多的改進優化, 可以說是 Guava 最為成熟/流行的模組之一。

陣列相關:Lists

集合相關:Sets

map 相關:Maps

連線符(Joiner)和分隔符(Splitter)

Joiner 做為連線符的使用非常簡單,下例是將 list 轉為使用連線符連線的字串:

List<Integer> list = Lists.newArrayList();
list.add(34);
list.add(64);
list.add(267);
list.add(865);

String result = Joiner.skipNulls().on("-").join(list);
System.out.println(result);

輸出:34-64-267-865

將 map 轉為自定義連線符連線的字串:

Map<String, Integer> map = Maps.newHashMap();
map.put("key1", 45);
map.put("key2",234);
String result = Joiner.on(",").withKeyValueSeparator("=").join(map);

System.out.println(result);

輸出:
key1=45,key2=234

分隔符 Splitter 的使用也很簡單:

String str = "1-2-3-4-5-6";
List<String> list = Splitter.on("-").splitToList(str);

System.out.println(list);

輸出:
[1, 2, 3, 4, 5, 6]

如果字串中帶有空格,還可以先去掉空格:

String str = "1-2-3-4-  5-  6   ";
List<String> list = Splitter.on("-").omitEmptyStrings().trimResults().splitToList(str);
System.out.println(list);

將 String 轉為 map:

String str = "key1=54,key2=28";
Map<String,String> map = Splitter.on(",").withKeyValueSeparator("=").split(str);
System.out.println(map);

輸出:
{key1=54, key2=28}

Comparator 的實現

Java 提供了 Comparator 可以用來對物件進行排序。Guava 提供了排序器 Ordering 類封裝了很多實用的操作。

Ordering 提供了一些有用的方法:

natural()	對可排序型別做自然排序,如數字按大小,日期按先後排序
usingToString()	按物件的字串形式做字典排序[lexicographical ordering]
from(Comparator)	把給定的Comparator轉化為排序器
reverse()	獲取語義相反的排序器
nullsFirst()	使用當前排序器,但額外把null值排到最前面。
nullsLast()	使用當前排序器,但額外把null值排到最後面。
compound(Comparator)	合成另一個比較器,以處理當前排序器中的相等情況。
lexicographical()	基於處理型別T的排序器,返回該型別的可迭代物件Iterable<T>的排序器。
onResultOf(Function)	對集合中元素呼叫Function,再按返回值用當前排序器排序。

示例:

UserInfo build = UserInfo.builder().uid(234L).gender(1).build();
UserInfo build1 = UserInfo.builder().uid(4354L).gender(0).build();


Ordering<UserInfo> byOrdering = Ordering.natural().nullsFirst().onResultOf((Function<UserInfo, Comparable<Integer>>) input -> input.getGender());
System.out.println(byOrdering.compare(build1, build));

build 的 gender 大於 build1 的,所以返回 -1,反之返回 1。

統計中間程式碼執行時間

Stopwatch 類提供了時間統計的功能,相當於幫你封裝了呼叫 System.currentTimeMillis() 的邏輯。

Stopwatch stopwatch = Stopwatch.createStarted();
try {
  //TODO 模擬業務邏輯
  Thread.sleep(2000L);
} catch (InterruptedException e) {
  e.printStackTrace();
}
long nanos = stopwatch.elapsed(TimeUnit.SECONDS);
System.out.println(nanos);

Guava Cache - 本地快取元件

Guava Cache 在日常的使用中非常地頻繁,甚至都沒有意識到這是第三方提供的工具類而是把它當成了 JDK 自帶的實現。

// LoadingCache是Cache的快取實現
LoadingCache<String, Object> cache = CacheBuilder.newBuilder()
  //設定快取大小
  .maximumSize(1000)
  //設定到期時間
  .expireAfterWrite(10, TimeUnit.MINUTES)
  //設定快取裡的值兩分鐘重新整理一次
  .refreshAfterWrite(2, TimeUnit.MINUTES)
  //開啟快取的統計功能
  .recordStats()
  //構建快取
  .build(new CacheLoader<String, Object>() {
    //此處實現如果根據key找不到value需要去如何獲取
    @Override
    public Object load(String s) throws Exception {
      return new Object();
    }

    //如果批量載入有比反覆呼叫load更優的方法則重寫這個方法
    @Override
    public Map<String, Object> loadAll(Iterable<? extends String> keys) throws Exception {
      return super.loadAll(keys);
    }
  });

設定本地快取使用 CacheBuilder.newBuilder(),支援設定快取大小,快取過期時間,快取重新整理頻率等等。如果你想統計快取的命中率, Guava Cache 也提供了這種能力幫你彙總當前快取是否有效。

同時快取如果因為某種原因未自動重新整理或者清除,Guava Cache 也支援使用者手動呼叫 API 重新整理或者清除快取。

cache.invalidateAll();//清除所有快取項
//清理的時機:在寫操作時順帶做少量的維護工作,或者偶爾在讀操作時做——如果寫操作實在太少的話
//如果想自己維護則可以呼叫Cache.cleanUp();
cache.cleanUp();
//另外有時候需要快取中的資料做出變化過載一次,這個過程可以非同步執行
cache.refresh("key");

單機限流工具類 - RateLimiter

常用的限流演算法有 漏桶演算法、令牌桶演算法。這兩種演算法各有側重點:

  • 漏桶演算法:漏桶的意思就像一個漏斗一樣,水一滴一滴的滴下去,流出是勻速的。當訪問量過大的時候這個漏斗就會積水。漏桶演算法的實現依賴佇列,一個處理器從隊頭依照固定頻率取出資料進行處理。如果請求量過大導致佇列堆滿那麼新來的請求就會被拋棄。漏桶一般按照固定的速率流出。
  • 令牌桶則是存放固定容量的令牌,按照固定速率從桶中取出令牌。初始給桶中新增固定容量令牌,當桶中令牌不夠取出的時候則拒絕新的請求。令牌桶不限制取出令牌的速度,只要有令牌就能處理。所以令牌桶允許一定程度的突發,而漏桶主要目的是平滑流出。

RateLimiter 使用了令牌桶演算法,提供兩種限流的實現方案:

  • 平滑突發限流(SmoothBursty)
  • 平滑預熱限流(SmoothWarmingUp)

實現平滑突發限流通過 RateLimiter 提供的靜態方法來建立:

RateLimiter r = RateLimiter.create(5);
while (true) {
  System.out.println("get 1 tokens: " + r.acquire() + "s");
}

輸出:
get 1 tokens: 0.0s
get 1 tokens: 0.197059s
get 1 tokens: 0.195338s
get 1 tokens: 0.196918s
get 1 tokens: 0.19955s
get 1 tokens: 0.199062s
get 1 tokens: 0.195589s
get 1 tokens: 0.195061s
......  

設定每秒放置的令牌數為 5 個,基本 0.2s 一次符合每秒 5 個的設定。保證每秒不超過 5 個達到了平滑輸出的效果。

在沒有請求使用令牌桶的時候,令牌會先建立好放在桶中,所以此時如果突然有突發流量進來,由於桶中有足夠的令牌可以快速響應。RateLimiter 在沒有足夠令牌發放時採用滯後處理的方式,前一個請求獲取令牌所需等待的時間由下一次請求來承受。

平滑預熱限流並不會像平滑突發限流一樣先將所有的令牌建立好,它啟動後會有一段預熱期,逐步將分發頻率提升到配置的速率。

比如下面例子建立一個平均分發令牌速率為 2,預熱期為 3 分鐘。由於設定了預熱時間是 3 秒,令牌桶一開始並不會 0.5 秒發一個令牌,而是形成一個平滑線性下降的坡度,頻率越來越高,在 3 秒鐘之內達到原本設定的頻率,以後就以固定的頻率輸出。這種功能適合系統剛啟動需要一點時間來“熱身”的場景。

RateLimiter r = RateLimiter.create(2, 3, TimeUnit.SECONDS);
while (true) {
  System.out.println("get 1 tokens: " + r.acquire(1) + "s");
  System.out.println("get 1 tokens: " + r.acquire(1) + "s");
  System.out.println("end");
}

輸出:
get 1 tokens: 0.0s
get 1 tokens: 1.33068s
end
get 1 tokens: 0.995792s
get 1 tokens: 0.662838s
end
get 1 tokens: 0.494775s
get 1 tokens: 0.497293s
end
get 1 tokens: 0.49966s
get 1 tokens: 0.49625s
end

從上面的輸出看前面兩次獲取令牌都很耗時,往後就越來越趨於平穩。

今天給大家介紹的常用的 Guava 工具類就這些,不過 JDK8 開始 Java官方 API 也在完善,比如像字串相關的功能 JDK也很強大。都是工具,哪個好用就用哪個。

相關文章