☕【Java技術指南】「Guava Collections」實戰使用相關Guava不一般的集合框架

李浩宇Alex發表於2021-08-22

Google Guava Collections 使用介紹

簡介

Google Guava Collections 是一個對 Java Collections Framework 增強和擴充套件的一個開源專案。由於它高質量 API 的實現和對 Java特性的充分利用,使得其在 Java 社群受到很高評價。筆者主要介紹它的基本用法和功能特性

使用介紹

  • Google Guava Collections(以下都簡稱為 Guava Collections)是 Java Collections Framework 的增強和擴充套件。每個 Java 開發者都會在工作中使用各種資料結構,很多情況下 Java Collections Framework 可以幫助你完成這類工作。

  • 但是在有些場合你使用了 Java Collections Framework 的 API,但還是需要寫很多程式碼來實現一些複雜邏輯, 這個時候就可以嘗試使用 Guava Collections 來幫助你完成這些工作。這些高質量的 API 使你的程式碼更短,更易於閱 讀和修改,工作更加輕鬆。

目標讀者

  • 對於理解 Java 開源工具來說,本文讀者至少應具備基礎的 Java 知識,特別是 JDK5 的特性。
  • 因為 Guava Collections 充分使用了範型,迴圈增強這樣的特性。作為 Java Collections Framework 的增強,讀者必須對 Java Collections Framework 有清晰的理解,包括主要的介面約定和常用的實現類。
  • 並且 Guava Collections 很大程度上是幫助開發者完成比較複雜的資料結構的操作,因此基礎的資料結構和演算法的知識也是清晰理解 Guava Collections 的 必要條件。

專案背景

  • Guava Collections 是 Google 的工程師 Kevin Bourrillion 和 Jared Levy 在著名"20%"時間寫的程式碼。當然作為開源專案還有其他的開發者貢獻了程式碼。

  • 在編寫的過程中,Java Collections Framework 的作者 Joshua Bloch 也參與了程式碼稽核和提出建議。

  • 目前它已經移到另外一個叫 guava-libraries 的開源專案下面來維護。 因為功能相似而且又同是開源專案,人們很很自然會把它和 Apache Commons Collections 來做比較。


集合介紹

  • Immutable Collections: 還在使用 Collections.unmodifiableXXX() ? Immutable Collections 這才是真正的不 可修改的集合 l Multiset: 看看如何把重複的元素放入一個集合。

  • Multimaps: 需要在一個 key 對應多個 value 的時候 , 自己寫一個實現比較繁瑣,讓 Multimaps 來幫忙

  • BiMap: java.util.Map 只能保證 key 的不重複,BiMap 保證 value 也不重複 l

  • MapMaker: 超級強大的 Map 構造類

  • Ordering class: 大家知道用 Comparator 作為比較器來對集合排序,但是對於多關鍵字排序 Ordering class 可以簡化很多的程式碼


Immutable Collections: 真正的不可修改的集合
  • 大家都用過Collections.unmodifiableXXX() 來做一個不可修改的集合。例如你要構造儲存常量的Set。
Set<String> set = new HashSet<String>(Arrays.asList(new String[]{"RED", "GREEN"})); 
Set<String> unmodifiableSet = Collections.unmodifiableSet(set);
  • 每次調 unmodifiableSet.add() 會丟擲個 UnsupportedOperationException。

  • 如果有人在原來的set上add或者remove元素會怎麼樣?結果unmodifiableSet也是被 add 或者 remove 元素了。

構造這樣一個簡單的set寫了兩句長的程式碼。下面看看ImmutableSet 是怎麼來做地更安全和簡潔 :

ImmutableSet<String> immutableSet = ImmutableSet.of("RED", "GREEN");
  • 而且試圖調 add 方法的時候,它一樣會丟擲 UnsupportedOperationException,重要的是程式碼的可讀性增強了不少,非常直觀地展現了程式碼的用意。如果像之前這個程式碼保護一個set 怎麼做呢?**
ImmutableSet<String> immutableSet = ImmutableSet.copyOf(set);

從構造的方式來說,ImmutableSet 集合還提供了 Builder 模式來構造一個集合 :在這個例子裡面 Builder 不但能加入單個元素還能加入既有的集合。

Builder<String> builder = ImmutableSet.builder(); 
ImmutableSet<String> immutableSet = builder.add("RED").addAll(set).build();

Guava Collections 還提供了各種 Immutable 集合的實現

  • ImmutableList

  • ImmutableSet

 ImmutableSet.of(4, 8, 15, 16, 23, 42);
 ImmutableSet.copyOf(numbers);
  • ImmutableSortedSet
  • ImmutableMap
public static final ImmutableMap<String, Integer>
 ENGLISH_TO_INT = ImmutableMap
 .with("four", 4)
 .with("eight", 8)
 .with("fifteen", 15)
 .with("sixteen", 16)
 .with("twenty-three", 23)
 .with("forty-two", 42)
 .build();

ImmutableMap.of(1, "one", 2, "two");
  • ImmutableSortedMap (one day)

Multiset: 把重複的元素放入集合
  • 你可能會說這和 Set 介面的契約衝突,因為Set介面的 JavaDoc 裡面規定不能放入重複元素。事實上,Multiset 並 沒有實現 java.util.Set 介面,它更像是一個Bag。普通的 Set 就像這樣 :[car, ship, bike],而 Multiset 會是這樣 :
    [car x 2, ship x 6, bike x 3]。

  • 譬如一個 List 裡面有各種字串,然後你要統計每個字串在 List 裡面出現的次數:

Map<String, Integer> map = new HashMap<String, Integer>(); 
for(String word : wordList){ 
   Integer count = map.get(word); 
   map.put(word, (count == null) ? 1 : count + 1); 
}
//count word “the” Integer count = map.get(“the”);
如果用 Multiset 就可以這樣 :
HashMultiset<String> multiSet = HashMultiset.create();
multiSet.addAll(wordList); 
//count word “the” Integer count = multiSet.count(“the”);
  • 這樣連迴圈都不用了,而且 Multiset 用的方法叫 count,顯然比在 Map 裡面調 get 有更好的可讀性。

  • Multiset 還提供了 setCount 這樣設定元素重複次數的方法,雖然你可以通過使用 Map 來實現類似的功能,但是程式的可讀性比 Multiset 差了很多。

常用實現 Multiset 介面的類有:
  • HashMultiset: 元素存放於HashMap
  • LinkedHashMultiset: 元素存放於 LinkedHashMap,即元素的排列順序由第一次放入的順序決定
  • TreeMultiset:元素被排序存放於TreeMap
  • EnumMultiset: 元素必須是 enum 型別
  • ImmutableMultiset: 不可修改的 Mutiset
  • 看到這裡你可能已經發現 Guava Collections 都是以 create 或是 of 這樣的靜態方法來構造物件。這是因為這些集合
  • 類大多有多個引數的私有構造方法,由於引數數目很多,客戶程式碼程式設計師使用起來就很不方便。而且以這種方式可以
  • 返回原型別的子型別物件。另外,對於建立範型物件來講,這種方式更加簡潔。

看到這裡你可能已經發現 Guava Collections 都是以 create 或是 of 這樣的靜態方法來構造物件。這是因為這些集合類大多有多個引數的私有構造方法,由於引數數目很多,客戶程式碼程式設計師使用起來就很不方便。而且以這種方式可以
返回原型別的子型別物件。另外,對於建立範型物件來講,這種方式更加簡潔。

Multimap: 在 Map 的 value 裡面放多個元素

Multimap就是一個 key 對應多個 value 的資料結構。看上去它很像 java.util.Map 的結構,但是 Muitimap 不是 Map,沒有實現 Map 的介面。設想你對 Map 調了 2 次引數 key 一樣的 put 方法,結果就是第 2 次的 value 覆蓋
了第 1 次的 value。但是對 Muitimap 來說這個 key 同時對應了 2 個 value。所以 Map 看上去是 : {k1=v1, k2=v2,...},而 Muitimap 是 :{k1=[v1, v2, v3], k2=[v7, v8],....}。

  • 舉個記名投票的例子,所有選票都放在一個 List 裡面,List 的每個元素包括投票人和選舉人的名字。

我們可以這樣寫 :

//Key is candidate name, its value is his voters 
HashMap<String, HashSet<String>> hMap = new HashMap<String, HashSet<String>>(); 
for(Ticket ticket: tickets){ 
HashSet<String> set = hMap.get(ticket.getCandidate()); 
if(set == null){ 
 set = new HashSet<String>(); 
 hMap.put(ticket.getCandidate(), set); 
} 
set.add(ticket.getVoter());
}

我們再來看看 Muitimap 能做些什麼 :

HashMultimap<String, String> map = HashMultimap.create(); 
for(Ticket ticket: tickets){ 
map.put(ticket.getCandidate(), ticket.getVoter()); 
}

就這麼簡單! Muitimap 介面的主要實現類有:

  • HashMultimap: key 放在 HashMap,而 value 放在 HashSet,即一個 key 對應的 value 不可重複
  • ArrayListMultimap: key 放在 HashMap,而 value 放在 ArrayList,即一個 key 對應的 value 有順序可重複
  • LinkedHashMultimap: key 放在 LinkedHashMap,而 value 放在 LinkedHashSet,即一個 key 對應的 value 有順序不可重複
  • TreeMultimap: key 放在 TreeMap,而 value 放在 TreeSet,即一個 key 對應的 value 有排列順序
  • ImmutableMultimap: 不可修改的 Multimap
BiMap: 雙向 Map

**BiMap 實現java.util.Map 介面。它的特點是它的value和它 key一樣也是不可重複的,換句話說它的 key 和value是等價的,如果你往 BiMap 的 value 裡面放了重複的元素,就會得到 IllegalArgumentException。 **

舉個例子,你可能經常會碰到在 Map 裡面根據 value 值來反推它的 key 值的邏輯:

for(Map.Entry<User, Address> entry : map.entreSet()){ 
if(entry.getValue().equals(anAddess)){ 
 return entry.getKey(); 
} 
} 
return null;
  • 如果把 User 和 Address 都放在 BiMap,那麼一句程式碼就得到結果了: return biMap.inverse().get(anAddess);
  • 這裡的 inverse 方法就是把 BiMap 的 key 集合 value 集合對調,因此 biMap == biMap.inverse().inverse()。
BiMap的常用實現有:
  • HashBiMap: key 集合與 value 集合都有 HashMap 實現
  • EnumBiMap: key 與 value 都必須是 enum 型別
  • ImmutableBiMap: 不可修改的 BiMap

MapMaker: 超級強大的 Map 構造工具

MapMaker 是用來構造 ConcurrentMap 的工具類。
為什麼可以把 MapMaker 叫做超級強大?看了下面的例子你就知道了。首先,它可以用來構造 ConcurrentHashMap:

//ConcurrentHashMap with concurrency level 8 
ConcurrentMap<String, Object> map1 = new MapMaker().concurrencyLevel(8).makeMap(); 

或者構造用各種不同 reference 作為 key 和 value 的 Map:

//ConcurrentMap with soft reference key and weak reference value 
ConcurrentMap<String, Object> map2 = new MapMaker().softKeys().weakValues().makeMap(); 

或者構造有自動移除時間過期項的 Map:

//Automatically removed entries from map after 30 seconds since they are created 
ConcurrentMap<String, Object> map3 = new MapMaker() 
.expireAfterWrite(30, TimeUnit.SECONDS)
.makeMap();

或者構造有最大限制數目的 Map:

//Map size grows close to the 100, the map will evict 
//entries that are less likely to be used again 
ConcurrentMap<String, Object> map4 = new MapMaker() 
.maximumSize(100)
.makeMap();

或者提供當 Map 裡面不包含所 get 的項,而需要自動加入到 Map 的功能。這個功能當 Map 作為快取的時候很有 用 :

//Create an Object to the map, when get() is missing in map 
ConcurrentMap<String, Object> map5 = new MapMaker().makeComputingMap( 
 new Function<String, Object>() { 
 public Object apply(String key) { 
 	return createObject(key); 
}});

這些還不是最強大的特性,最厲害的是 MapMaker 可以提供擁有以上所有特性的 Map:

//Put all features together! 
ConcurrentMap<String, Object> mapAll = new MapMaker() 
.concurrencyLevel(8) 
.softKeys() 
.weakValues() 
.expireAfterWrite(30, TimeUnit.SECONDS) 
.maximumSize(100) 
.makeComputingMap( 
 new Function<String, Object>() { 
 public Object apply(String key) { 
 return createObject(key); 
 }});
Ordering class: 靈活的多欄位排序比較器

要對集合排序或者求最大值最小值,首推 java.util.Collections 類,但關鍵是要提供 Comparator 介面的實現。假設有 個待排序的 List,而 Foo 裡面有兩個排序關鍵字 int a, int b 和 int c:

Collections.sort(list, new Comparator<Foo>(){ 
@Override 
public int compare(Foo f1, Foo f2) { 
 	int resultA = f1.a – f2.a; 
 	int resultB = f1.b – f2.b; 
 	return resultA == 0 ? (resultB == 0 ? f1.c – f2.c : resultB) : resultA;
}});

這看上去有點眼暈,如果用一串 if-else 也好不到哪裡去。看看 ComparisonChain 能做到什麼 :

Collections.sort(list, new Comparator<Foo>(){ 
@Override 
return ComparisonChain.start() 
 .compare(f1.a, f2.a) 
 .compare(f1.b, f2.b) 
 .compare(f1.c, f2.c).result();
 }});

如果排序關鍵字要用自定義比較器,compare 方法也有接受 Comparator 的過載版本。譬如 Foo 裡面每個排序關鍵字都已經有了各自的 Comparator,那麼利用 ComparisonChain 可以 :

Collections.sort(list, new Comparator<Foo>(){ 
@Override 
return ComparisonChain.start() 
 .compare(f1.a, f2.a, comparatorA) 
 .compare(f1.b, f2.b, comparatorB) 
 .compare(f1.c, f2.c, comparatorC).result(); 
 }});

Ordring 類還提供了一個組合 Comparator 物件的方法。而且 Ordring 本身實現了 Comparator 介面所以它能直接作 為 Comparator 使用:

Ordering<Foo> ordering = Ordering.compound(Arrays.asList(comparatorA, comparatorB, comparatorc)); 
Collections.sort(list, ordering);
過濾器(stream-filter)

利用 Collections2.filter() 方法過濾集合中不符合條件的元素。譬如過濾一個 List 裡面小於 10 的
元素 :

Collection<Integer> filterCollection = 
 Collections2.filter(list, new Predicate<Integer>(){ 
@Override 
public boolean apply(Integer input) { 
 return input >= 10; 
}});
  • 當然,你可以自己寫一個迴圈來實現這個功能,但是這樣不能保證之後小於 10 的元素不被放入集合。

  • filter 的強大之 處在於返回的 filterCollection 仍然有排斥小於 10 的元素的特性,如果調 filterCollection.add(9) 就會得到一個IllegalArgumentException。


轉換器(Stream-map)

利用 Collections2.transform() 方法來轉換集合中的元素。譬如把一個 Set 裡面所有元素都轉換成 帶格式的 String 來產生新的 Collection:

Collection<String> formatCollection = 
 Collections2.transform(set, new Function<Integer, String>(){ 
@Override 
public String apply(Integer input) { 
 return new DecimalFormat("#,###").format(input); 
}} );

總結

以上介紹了 Guava Collections 的一些基本的功能特性。你可以從 guava-libraries 的官方網站下載它的 jar 包和它其他的相關文件。如果你使用 Maven 來管理你的專案依賴包,Maven 中央庫也提供了它版本的依賴。最後希望Guava Collections 使你的程式設計工作更輕鬆,更有樂趣。

使用

這個開源專案釋出的 jar 包可以在它的官方網站內(http://code.google.com/p/guava-libraries/downloads/list)找到。

其下載的 zip 包中含有 Guava Collections 的 jar 包 guava-r09.jar 及其依賴包 guava-r09-gwt.jar,javadoc,原始碼,readme 等檔案。使用時只需將 guava-r09.jar 和依賴包 guava-r09-gwt.jar 放入 CLASSPATH 中即可。

如果您使用 Maven 作為構建工具的話可以在 pom.xml 內加入:

<dependency> 
<groupId>com.google.guava</groupId> 
<artifactId>guava</artifactId> 
<version>xxx</version> 
</dependency> 

相關文章