Guava 引入了很多 JDK 沒有的、但我們發(fā)現(xiàn)明顯有用的新集合類型。這些新類型是為了和 JDK 集合框架共存,而沒有往 JDK 集合抽象中硬塞其他概念。作為一般規(guī)則,Guava 集合非常精準(zhǔn)地遵循了 JDK 接口契約。
統(tǒng)計(jì)一個(gè)詞在文檔中出現(xiàn)了多少次,傳統(tǒng)的做法是這樣的:
Map<String, Integer> counts = new HashMap<String, Integer>();
for (String word : words) {
Integer count = counts.get(word);
if (count == null) {
counts.put(word, 1);
} else {
counts.put(word, count + 1);
}
}
這種寫法很笨拙,也容易出錯(cuò),并且不支持同時(shí)收集多種統(tǒng)計(jì)信息,如總詞數(shù)。我們可以做的更好。
Guava 提供了一個(gè)新集合類型 Multiset,它可以多次添加相等的元素。維基百科從數(shù)學(xué)角度這樣定義 Multiset:”集合[set]概念的延伸,它的元素可以重復(fù)出現(xiàn)…與集合[set]相同而與元組[tuple]相反的是,Multiset 元素的順序是無關(guān)緊要的:Multiset {a, a, b}和{a, b, a}是相等的”。——譯者注:這里所說的集合[set]是數(shù)學(xué)上的概念,Multiset繼承自 JDK 中的 Collection 接口,而不是 Set 接口,所以包含重復(fù)元素并沒有違反原有的接口契約。
可以用兩種方式看待 Multiset:
Guava 的 Multiset API 也結(jié)合考慮了這兩種方式:
當(dāng)把 Multiset 看成普通的 Collection 時(shí),它表現(xiàn)得就像無序的 ArrayList:
當(dāng)把 Multiset 看作 Map<E, Integer>時(shí),它也提供了符合性能期望的查詢操作:
值得注意的是,除了極少數(shù)情況,Multiset 和 JDK 中原有的 Collection 接口契約完全一致——具體來說,TreeMultiset 在判斷元素是否相等時(shí),與 TreeSet 一樣用 compare,而不是 Object.equals。另外特別注意,Multiset.addAll(Collection)可以添加 Collection 中的所有元素并進(jìn)行計(jì)數(shù),這比用 for 循環(huán)往 Map 添加元素和計(jì)數(shù)方便多了。
方法 | 描述 |
count(E) | 給定元素在 Multiset 中的計(jì)數(shù) |
elementSet() | Multiset 中不重復(fù)元素的集合,類型為 Set<E> |
entrySet() | 和 Map 的 entrySet 類似,返回 Set<Multiset.Entry<E>>,其中包含的 Entry 支持 getElement()和 getCount()方法 |
add(E, int) | 增加給定元素在 Multiset 中的計(jì)數(shù) |
remove(E, int) | 減少給定元素在 Multiset 中的計(jì)數(shù) |
setCount(E, int) | 設(shè)置給定元素在 Multiset 中的計(jì)數(shù),不可以為負(fù)數(shù) |
size() | 返回集合元素的總個(gè)數(shù)(包括重復(fù)的元素) |
請(qǐng)注意,Multiset
Guava 提供了多種 Multiset 的實(shí)現(xiàn),大致對(duì)應(yīng) JDK 中 Map 的各種實(shí)現(xiàn):
Map | 對(duì)應(yīng)的Multiset | 是否支持null元素 |
HashMap | HashMultiset | 是 |
TreeMap | TreeMultiset | 是(如果 comparator 支持的話) |
LinkedHashMap | LinkedHashMultiset | 是 |
ConcurrentHashMap | ConcurrentHashMultiset | 否 |
ImmutableMap | ImmutableMultiset | 否 |
SortedMultiset 是 Multiset 接口的變種,它支持高效地獲取指定范圍的子集。比方說,你可以用 latencies.subMultiset(0,BoundType.CLOSED, 100, BoundType.OPEN).size()來統(tǒng)計(jì)你的站點(diǎn)中延遲在 100 毫秒以內(nèi)的訪問,然后把這個(gè)值和 latencies.size()相比,以獲取這個(gè)延遲水平在總體訪問中的比例。
TreeMultiset 實(shí)現(xiàn) SortedMultiset 接口。在撰寫本文檔時(shí),ImmutableSortedMultiset 還在測(cè)試和 GWT 的兼容性。
每個(gè)有經(jīng)驗(yàn)的 Java 程序員都在某處實(shí)現(xiàn)過 Map<K, List
可以用兩種方式思考 Multimap 的概念:”鍵-單個(gè)值映射”的集合:
a -> 1 a -> 2 a ->4 b -> 3 c -> 5
或者”鍵-值集合映射”的映射:
a -> [1, 2, 4] b -> 3 c -> 5
一般來說,Multimap 接口應(yīng)該用第一種方式看待,但 asMap()視圖返回 Map<K, Collection
很少會(huì)直接使用 Multimap 接口,更多時(shí)候你會(huì)用 ListMultimap 或 SetMultimap 接口,它們分別把鍵映射到 List 或 Set。
Multimap.get(key)以集合形式返回鍵所對(duì)應(yīng)的值視圖,即使沒有任何對(duì)應(yīng)的值,也會(huì)返回空集合。ListMultimap.get(key)返回 List,SetMultimap.get(key)返回 Set。
對(duì)值視圖集合進(jìn)行的修改最終都會(huì)反映到底層的 Multimap。例如:
Set<Person> aliceChildren = childrenMultimap.get(alice);
aliceChildren.clear();
aliceChildren.add(bob);
aliceChildren.add(carol);
其他(更直接地)修改 Multimap 的方法有:
方法簽名 | 描述 | 等價(jià)于 |
put(K, V) | 添加鍵到單個(gè)值的映射 | multimap.get(key).add(value) |
putAll(K, Iterable<V>) | 依次添加鍵到多個(gè)值的映射 | Iterables.addAll(multimap.get(key), values) |
remove(K, V) | 移除鍵到值的映射;如果有這樣的鍵值并成功移除,返回 true。 | multimap.get(key).remove(value) |
removeAll(K) | 清除鍵對(duì)應(yīng)的所有值,返回的集合包含所有之前映射到 K 的值,但修改這個(gè)集合就不會(huì)影響 Multimap 了。 | multimap.get(key).clear() |
replaceValues(K, Iterable<V>) | 清除鍵對(duì)應(yīng)的所有值,并重新把 key 關(guān)聯(lián)到 Iterable 中的每個(gè)元素。返回的集合包含所有之前映射到 K 的值。 | multimap.get(key).clear(); Iterables.addAll(multimap.get(key), values) |
Multimap 還支持若干強(qiáng)大的視圖:
Multimap<K, V>不是 Map<K,Collection
Multimap 提供了多種形式的實(shí)現(xiàn)。在大多數(shù)要使用 Map<K, Collection
實(shí)現(xiàn) | 鍵行為類似 | 值行為類似 |
ArrayListMultimap | HashMap | ArrayList |
HashMultimap | HashMap | HashSet |
LinkedListMultimap* | LinkedHashMap* | LinkedList* |
LinkedHashMultimap** | LinkedHashMap | LinkedHashMap |
TreeMultimap | TreeMap | TreeSet |
ImmutableListMultimap | ImmutableMap | ImmutableList |
ImmutableSetMultimap | ImmutableMap | ImmutableSet |
除了兩個(gè)不可變形式的實(shí)現(xiàn),其他所有實(shí)現(xiàn)都支持 null 鍵和 null 值
*LinkedListMultimap.entries()保留了所有鍵和值的迭代順序。詳情見 doc 鏈接。
**LinkedHashMultimap 保留了映射項(xiàng)的插入順序,包括鍵插入的順序,以及鍵映射的所有值的插入順序。
請(qǐng)注意,并非所有的 Multimap 都和上面列出的一樣,使用 Map<K, Collection
如果你想要更大的定制化,請(qǐng)用 Multimaps.newMultimap(Map, Supplier
傳統(tǒng)上,實(shí)現(xiàn)鍵值對(duì)的雙向映射需要維護(hù)兩個(gè)單獨(dú)的 map,并保持它們間的同步。但這種方式很容易出錯(cuò),而且對(duì)于值已經(jīng)在 map 中的情況,會(huì)變得非常混亂。例如:
Map<String, Integer> nameToId = Maps.newHashMap();
Map<Integer, String> idToName = Maps.newHashMap();
nameToId.put("Bob", 42);
idToName.put(42, "Bob");
//如果"Bob"和42已經(jīng)在map中了,會(huì)發(fā)生什么?
//如果我們忘了同步兩個(gè)map,會(huì)有詭異的bug發(fā)生...
BiMap<K, V>是特殊的 Map:
在 BiMap 中,如果你想把鍵映射到已經(jīng)存在的值,會(huì)拋出 IllegalArgumentException 異常。如果對(duì)特定值,你想要強(qiáng)制替換它的鍵,請(qǐng)使用 BiMap.forcePut(key, value)。
BiMap<String, Integer> userId = HashBiMap.create();
...
String userForId = userId.inverse().get(id);
鍵–值實(shí)現(xiàn) | 值–鍵實(shí)現(xiàn) | 對(duì)應(yīng)的BiMap實(shí)現(xiàn) |
HashMap | HashMap | HashBiMap |
ImmutableMap | ImmutableMap | ImmutableBiMap |
EnumMap | EnumMap | EnumBiMap |
EnumMap | HashMap | EnumHashBiMap |
注:Maps 類中還有一些諸如 synchronizedBiMap 的 BiMap 工具方法.
Table<Vertex, Vertex, Double> weightedGraph = HashBasedTable.create();
weightedGraph.put(v1, v2, 4);
weightedGraph.put(v1, v3, 20);
weightedGraph.put(v2, v3, 5);
weightedGraph.row(v1); // returns a Map mapping v2 to 4, v3 to 20
weightedGraph.column(v3); // returns a Map mapping v1 to 20, v2 to 5
通常來說,當(dāng)你想使用多個(gè)鍵做索引的時(shí)候,你可能會(huì)用類似 Map<FirstName, Map<LastName, Person>>的實(shí)現(xiàn),這種方式很丑陋,使用上也不友好。Guava 為此提供了新集合類型 Table,它有兩個(gè)支持所有類型的鍵:”行”和”列”。Table 提供多種視圖,以便你從各種角度使用它:
Table 有如下幾種實(shí)現(xiàn):
ClassToInstanceMap 是一種特殊的 Map:它的鍵是類型,而值是符合鍵所指類型的對(duì)象。
為了擴(kuò)展 Map 接口,ClassToInstanceMap 額外聲明了兩個(gè)方法:T getInstance(Class
ClassToInstanceMap 有唯一的泛型參數(shù),通常稱為 B,代表 Map 支持的所有類型的上界。例如:
ClassToInstanceMap<Number> numberDefaults=MutableClassToInstanceMap.create();
numberDefaults.putInstance(Integer.class, Integer.valueOf(0));
從技術(shù)上講,從技術(shù)上講,ClassToInstanceMap<B> 實(shí)現(xiàn)了 Map<Class<? extends B>, B>——或者換句話說,是一個(gè)映射 B 的子類型到對(duì)應(yīng)實(shí)例的 Map。這讓 ClassToInstanceMap 包含的泛型聲明有點(diǎn)令人困惑,但請(qǐng)記住 B 始終是 Map 所支持類型的上界——通常 B 就是 Object。
對(duì)于 ClassToInstanceMap,Guava 提供了兩種有用的實(shí)現(xiàn):MutableClassToInstanceMap 和 ImmutableClassToInstanceMap。
RangeSet描述了一組不相連的、非空的區(qū)間。當(dāng)把一個(gè)區(qū)間添加到可變的RangeSet時(shí),所有相連的區(qū)間會(huì)被合并,空區(qū)間會(huì)被忽略。例如:
RangeSet<Integer> rangeSet = TreeRangeSet.create();
rangeSet.add(Range.closed(1, 10)); // {[1,10]}
rangeSet.add(Range.closedOpen(11, 15));//不相連區(qū)間:{[1,10], [11,15)}
rangeSet.add(Range.closedOpen(15, 20)); //相連區(qū)間; {[1,10], [11,20)}
rangeSet.add(Range.openClosed(0, 0)); //空區(qū)間; {[1,10], [11,20)}
rangeSet.remove(Range.open(5, 10)); //分割[1, 10]; {[1,5], [10,10], [11,20)}
請(qǐng)注意,要合并 Range.closed(1, 10)和 Range.closedOpen(11, 15)這樣的區(qū)間,你需要首先用 Range.canonical(DiscreteDomain)對(duì)區(qū)間進(jìn)行預(yù)處理,例如 DiscreteDomain.integers()。
注:RangeSet不支持 GWT,也不支持 JDK5 和更早版本;因?yàn)椋琑angeSet 需要充分利用 JDK6 中 NavigableMap 的特性。
RangeSet 的實(shí)現(xiàn)支持非常廣泛的視圖:
為了方便操作,RangeSet 直接提供了若干查詢方法,其中最突出的有:
RangeMap 描述了”不相交的、非空的區(qū)間”到特定值的映射。和 RangeSet 不同,RangeMap 不會(huì)合并相鄰的映射,即便相鄰的區(qū)間映射到相同的值。例如:
RangeMap<Integer, String> rangeMap = TreeRangeMap.create();
rangeMap.put(Range.closed(1, 10), "foo"); //{[1,10] => "foo"}
rangeMap.put(Range.open(3, 6), "bar"); //{[1,3] => "foo", (3,6) => "bar", [6,10] => "foo"}
rangeMap.put(Range.open(10, 20), "foo"); //{[1,3] => "foo", (3,6) => "bar", [6,10] => "foo", (10,20) => "foo"}
rangeMap.remove(Range.closed(5, 11)); //{[1,3] => "foo", (3,5) => "bar", (11,20) => "foo"}
RangeMap 提供兩個(gè)視圖: