Google Guava Collections 使用介紹
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 來做比較。其區別歸結起來有以下幾點:
Guava Collections 充分利用了 JDK5 的范型和枚舉這樣的特性,而 Apache Commons Collections 則是基于 JDK1.2。其次 Guava Collections 更加嚴格遵守 Java Collections Framework 定義的接口契約,而在 Apache Commons Collections 你會發現不少違反這些 JDK 接口契約的地方。這些不遵守標準的地方就是出 bug 的風險很高。最后 Guava Collections 處于積極的維護狀態,本文介紹的特性都基于目前最新 2011 年 4 月的 Guava r09 版本,而 Apache Commons Collections 最新一次發布也已經是 2008 年了。
可以說 Java Collections Framework 滿足了我們大多數情況下使用集合的要求,但是當遇到一些特殊的情況我們的代碼會比較冗長,比較容易出錯。Guava Collections 可以幫助你的代碼更簡短精煉,更重要是它增強了代碼的可讀性。看看 Guava Collections 為我們做了哪些很酷的事情。
- Immutable Collections: 還在使用 Collections.unmodifiableXXX() ? Immutable Collections 這才是真正的不可修改的集合
- Multiset: 看看如何把重復的元素放入一個集合
- Multimaps: 需要在一個 key 對應多個 value 的時候 , 自己寫一個實現比較繁瑣 - 讓 Multimaps 來幫忙
- BiMap: java.util.Map 只能保證 key 的不重復,BiMap 保證 value 也不重復
- MapMaker: 超級強大的 Map 構造類
- Ordering class: 大家知道用 Comparator 作為比較器來對集合排序,但是對于多關鍵字排序 Ordering class 可以簡化很多的代碼
- 其他特性
當然,如果沒有 Guava Collections 你也可以用 Java Collections Framework 完成上面的功能。但是 Guava Collections 提供的這些 API 經過精心設計,而且還有 25000 個單元測試來保障它的質量。所以我們沒必要重新發明輪子。接下來我們來詳細看看 Guava Collections 的一些具體功能。
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<String> builder = ImmutableSet.builder(); ImmutableSet<String> immutableSet = builder.add("RED").addAll(set).build(); |
在這個例子里面 Builder 不但能加入單個元素還能加入既有的集合。
除此之外,Guava Collections 還提供了各種 Immutable 集合的實現:ImmutableList,ImmutableMap,ImmutableSortedSet,ImmutableSortedMap。
你可能會說這和 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 這樣的靜態方法來構造對象。這是因為這些集合類大多有多個參數的私有構造方法,由于參數數目很多,客戶代碼程序員使用起來就很不方便。而且以這種方式可以 返回原類型的子類型對象。另外,對于創建范型對象來講,這種方式更加簡潔。
Multimap: 在 Map 的 value 里面放多個元素
Muitimap 就是一個 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<Ticket> 里面,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 實現了 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 是用來構造 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); }}); |
要對集合排序或者求最大值最小值,首推 java.util.Collections 類,但關鍵是要提供 Comparator 接口的實現。假設有個待排序的 List<Foo>,而 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); |
過濾器:利用 Collections2.filter() 方法過濾集合中不符合條件的元素。譬如過濾一個 List<Integer> 里面小于 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。
轉換器:利用 Collections2.transform() 方法來轉換集合中的元素。譬如把一個 Set<Integer> 里面所有元素都轉換成帶格式的 String 來產生新的 Collection<String>:
Collection<String> formatCollection = Collections2.transform(set, new Function<Integer, String>(){ @Override public String apply(Integer input) { return new DecimalFormat("#,###").format(input); }} ); |
這個開源項目發布的 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>r09</version> </dependency> |
需要注意的是本文介紹的 Guava r09 需要 1.5 或者更高版本的 JDK。
以上介紹了 Guava Collections 的一些基本的功能特性。你可以從 guava-libraries 的官方網站下載它的 jar 包和它其他的相關文檔。如果你使用 Maven 來管理你的項目依賴包,Maven 中央庫也提供了它版本的依賴。最后希望 Guava Collections 使你的編程工作更輕松,更有樂趣。
<br />
學習
-
guava-libraries:這是 Guava Collections 的官方網站,上面有項目介紹,發布版本的信息,下載,wiki,issue,和代碼庫信息。
-
What is the Google Collections Library?(Geertjan Wielenga):這是 javalobby 對 Guava Collections 主要開發者 Kevin Bourrillion 和 Jared Levy 的采訪。
-
Google Collections 1.0 Offers Enhanced Implementations of the Java Collections Framework(Josh Long, 2010 年 1 月):這篇文章對 Guava Collections 的前身 Google Collections 做了一個簡短的介紹。
-
Stackoverflow 論壇上對于 Guava 的問題和解答:Stackoverflow 論壇上有專門關于 Guava Collections 的主題,上面有經常碰到的問題和解決方法。
- “關于 Java Collections API 您不知道的 5 件事,第 1 部分”
(developerWorks,2010 年 5 月):Java Collections API
遠不止是數組的替代品,雖然一開始這樣用也不錯。Ted Neward 提供了關于用 Collections 做更多事情的 5
個技巧,包括關于定制和擴展 Java Collections API 的基礎。
- “關于 Java Collections API 您不知道的 5 件事,第 2 部分” (developerWorks,2010 年 6 月):您可以在任何地方使用 Java 集合,但是一定要小心。集合有很多秘密,如果不正確處理可能會帶來麻煩。Ted 探索了 Java Collections API 復雜、多變的一面并為您提供了一些技巧,幫您充分利用 Iterable、HashMap 和 SortedSet,又不會帶來 bug。 </ul>