1. 程式人生 > >[Google Guava] 2.2-新集合型別

[Google Guava] 2.2-新集合型別

原文連結 譯文連結 譯者:沈義揚,校對:丁一

Guava引入了很多JDK沒有的、但我們發現明顯有用的新集合型別。這些新型別是為了和JDK集合框架共存,而沒有往JDK集合抽象中硬塞其他概念。作為一般規則,Guava集合非常精準地遵循了JDK介面契約。

Multiset

統計一個詞在文件中出現了多少次,傳統的做法是這樣的:

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);
    }
}

這種寫法很笨拙,也容易出錯,並且不支援同時收集多種統計資訊,如總詞數。我們可以做的更好。

Guava提供了一個新集合型別 Multiset,它可以多次新增相等的元素。維基百科從數學角度這樣定義Multiset:”集合[set]概念的延伸,它的元素可以重複出現…與集合[set]相同而與元組[tuple]相反的是,Multiset元素的順序是無關緊要的:Multiset {a, a, b}和{a, b, a}是相等的”。——譯者注:這裡所說的集合[set]是數學上的概念,Multiset繼承自JDK中的Collection介面,而不是Set介面,所以包含重複元素並沒有違反原有的介面契約。

可以用兩種方式看待Multiset:

  • 沒有元素順序限制的ArrayList<E>
  • Map<E, Integer>,鍵為元素,值為計數

Guava的Multiset API也結合考慮了這兩種方式:
當把Multiset看成普通的Collection時,它表現得就像無序的ArrayList:

  • add(E)新增單個給定元素
  • iterator()返回一個迭代器,包含Multiset的所有元素(包括重複的元素)
  • size()返回所有元素的總個數(包括重複的元素)

當把Multiset看作Map<E, Integer>時,它也提供了符合效能期望的查詢操作:

  • count(Object)返回給定元素的計數。HashMultiset.count的複雜度為O(1),TreeMultiset.count的複雜度為O(log n)。
  • entrySet()返回Set<Multiset.Entry<E>>,和Map的entrySet類似。
  • elementSet()返回所有不重複元素的Set<E>,和Map的keySet()類似。
  • 所有Multiset實現的記憶體消耗隨著不重複元素的個數線性增長。

值得注意的是,除了極少數情況,Multiset和JDK中原有的Collection介面契約完全一致——具體來說,TreeMultiset在判斷元素是否相等時,與TreeSet一樣用compare,而不是Object.equals。另外特別注意,Multiset.addAll(Collection)可以新增Collection中的所有元素並進行計數,這比用for迴圈往Map新增元素和計數方便多了。

方法 描述
給定元素在Multiset中的計數
Multiset中不重複元素的集合,型別為Set<E>
和Map的entrySet類似,返回Set<Multiset.Entry<E>>,其中包含的Entry支援getElement()和getCount()方法
增加給定元素在Multiset中的計數
設定給定元素在Multiset中的計數,不可以為負數
size() 返回集合元素的總個數(包括重複的元素)

Multiset不是Map

請注意,Multiset<E>不是Map<E, Integer>,雖然Map可能是某些Multiset實現的一部分。準確來說Multiset是一種Collection型別,並履行了Collection介面相關的契約。關於Multiset和Map的顯著區別還包括:

  • Multiset中的元素計數只能是正數。任何元素的計數都不能為負,也不能是0。elementSet()和entrySet()檢視中也不會有這樣的元素。
  • multiset.size()返回集合的大小,等同於所有元素計數的總和。對於不重複元素的個數,應使用elementSet().size()方法。(因此,add(E)把multiset.size()增加1)
  • multiset.iterator()會迭代重複元素,因此迭代長度等於multiset.size()。
  • Multiset支援直接增加、減少或設定元素的計數。setCount(elem, 0)等同於移除所有elem。
  • 對multiset 中沒有的元素,multiset.count(elem)始終返回0。

Multiset的各種實現

Guava提供了多種Multiset的實現,大致對應JDK中Map的各種實現:

SortedMultiset

SortedMultiset是Multiset 介面的變種,它支援高效地獲取指定範圍的子集。比方說,你可以用 latencies.subMultiset(0,BoundType.CLOSED, 100, BoundType.OPEN).size()來統計你的站點中延遲在100毫秒以內的訪問,然後把這個值和latencies.size()相比,以獲取這個延遲水平在總體訪問中的比例。

TreeMultiset實現SortedMultiset介面。在撰寫本文件時,ImmutableSortedMultiset還在測試和GWT的相容性。

Multimap

每個有經驗的Java程式設計師都在某處實現過Map<K, List<V>>或Map<K, Set<V>>,並且要忍受這個結構的笨拙。例如,Map<K, Set<V>>通常用來表示非標定有向圖。Guava的 Multimap可以很容易地把一個鍵對映到多個值。換句話說,Multimap是把鍵對映到任意多個值的一般方式。

可以用兩種方式思考Multimap的概念:”鍵-單個值對映”的集合:

a -> 1 a -> 2 a ->4 b -> 3 c -> 5

或者”鍵-值集合對映”的對映:

a -> [1, 2, 4] b -> 3 c -> 5

一般來說,Multimap介面應該用第一種方式看待,但asMap()檢視返回Map<K, Collection<V>>,讓你可以按另一種方式看待Multimap。重要的是,不會有任何鍵對映到空集合:一個鍵要麼至少到一個值,要麼根本就不在Multimap中。

很少會直接使用Multimap介面,更多時候你會用ListMultimap或SetMultimap介面,它們分別把鍵對映到List或Set。

修改Multimap

Multimap.get(key)以集合形式返回鍵所對應的值檢視,即使沒有任何對應的值,也會返回空集合。ListMultimap.get(key)返回List,SetMultimap.get(key)返回Set。

對值檢視集合進行的修改最終都會反映到底層的Multimap。例如:

Set<Person> aliceChildren = childrenMultimap.get(alice);
aliceChildren.clear();
aliceChildren.add(bob);
aliceChildren.add(carol);

其他(更直接地)修改Multimap的方法有:

方法簽名 描述 等價於
新增鍵到單個值的對映 multimap.get(key).add(value)
依次新增鍵到多個值的對映 Iterables.addAll(multimap.get(key), values)
移除鍵到值的對映;如果有這樣的鍵值併成功移除,返回true。 multimap.get(key).remove(value)
清除鍵對應的所有值,返回的集合包含所有之前對映到K的值,但修改這個集合就不會影響Multimap了。 multimap.get(key).clear()
清除鍵對應的所有值,並重新把key關聯到Iterable中的每個元素。返回的集合包含所有之前對映到K的值。 multimap.get(key).clear(); Iterables.addAll(multimap.get(key), values)

Multimap的檢視

Multimap還支援若干強大的檢視:

  • asMap為Multimap<K, V>提供Map<K,Collection<V>>形式的檢視。返回的Map支援remove操作,並且會反映到底層的Multimap,但它不支援put或putAll操作。更重要的是,如果你想為Multimap中沒有的鍵返回null,而不是一個新的、可寫的空集合,你就可以使用asMap().get(key)。(你可以並且應當把asMap.get(key)返回的結果轉化為適當的集合型別——如SetMultimap.asMap.get(key)的結果轉為Set,ListMultimap.asMap.get(key)的結果轉為List——Java型別系統不允許ListMultimap直接為asMap.get(key)返回List——譯者注:也可以用Multimaps中的asMap靜態方法幫你完成型別轉換
  • entries用Collection<Map.Entry<K, V>>返回Multimap中所有”鍵-單個值對映”——包括重複鍵。(對SetMultimap,返回的是Set)
  • keySet用Set表示Multimap中所有不同的鍵。
  • keys用Multiset表示Multimap中的所有鍵,每個鍵重複出現的次數等於它對映的值的個數。可以從這個Multiset中移除元素,但不能做新增操作;移除操作會反映到底層的Multimap。
  • values()用一個”扁平”的Collection<V>包含Multimap中的所有值。這有一點類似於Iterables.concat(multimap.asMap().values()),但它直接返回了單個Collection,而不像multimap.asMap().values()那樣是按鍵區分開的Collection。

Multimap不是Map

Multimap<K, V>不是Map<K,Collection<V>>,雖然某些Multimap實現中可能使用了map。它們之間的顯著區別包括:

  • Multimap.get(key)總是返回非null、但是可能空的集合。這並不意味著Multimap為相應的鍵花費記憶體建立了集合,而只是提供一個集合檢視方便你為鍵增加對映值——譯者注:如果有這樣的鍵,返回的集合只是包裝了Multimap中已有的集合;如果沒有這樣的鍵,返回的空集合也只是持有Multimap引用的棧物件,讓你可以用來操作底層的Multimap。因此,返回的集合不會佔據太多記憶體,資料實際上還是存放在Multimap中。
  • 如果你更喜歡像Map那樣,為Multimap中沒有的鍵返回null,請使用asMap()檢視獲取一個Map<K, Collection<V>>。(或者用靜態方法Multimaps.asMap()為ListMultimap返回一個Map<K, List<V>>。對於SetMultimap和SortedSetMultimap,也有類似的靜態方法存在)
  • 當且僅當有值對映到鍵時,Multimap.containsKey(key)才會返回true。尤其需要注意的是,如果鍵k之前對映過一個或多個值,但它們都被移除後,Multimap.containsKey(key)會返回false。
  • Multimap.entries()返回Multimap中所有”鍵-單個值對映”——包括重複鍵。如果你想要得到所有”鍵-值集合對映”,請使用asMap().entrySet()。
  • Multimap.size()返回所有”鍵-單個值對映”的個數,而非不同鍵的個數。要得到不同鍵的個數,請改用Multimap.keySet().size()。

Multimap的各種實現

Multimap提供了多種形式的實現。在大多數要使用Map<K, Collection<V>>的地方,你都可以使用它們:

除了兩個不可變形式的實現,其他所有實現都支援null鍵和null值

*LinkedListMultimap.entries()保留了所有鍵和值的迭代順序。詳情見doc連結。

**LinkedHashMultimap保留了對映項的插入順序,包括鍵插入的順序,以及鍵對映的所有值的插入順序。

請注意,並非所有的Multimap都和上面列出的一樣,使用Map<K, Collection<V>>來實現(特別是,一些Multimap實現用了自定義的hashTable,以最小化開銷)

BiMap

傳統上,實現鍵值對的雙向對映需要維護兩個單獨的map,並保持它們間的同步。但這種方式很容易出錯,而且對於值已經在map中的情況,會變得非常混亂。例如:

Map<String, Integer> nameToId = Maps.newHashMap();
Map<Integer, String> idToName = Maps.newHashMap();

nameToId.put("Bob", 42);
idToName.put(42, "Bob");
//如果"Bob"和42已經在map中了,會發生什麼?
//如果我們忘了同步兩個map,會有詭異的bug發生...
  • 可以用 inverse()反轉BiMap<K, V>的鍵值對映
  • 保證值是唯一的,因此 values()返回Set而不是普通的Collection

在BiMap中,如果你想把鍵對映到已經存在的值,會丟擲IllegalArgumentException異常。如果對特定值,你想要強制替換它的鍵,請使用 BiMap.forcePut(key, value)

BiMap<String, Integer> userId = HashBiMap.create();
...

String userForId = userId.inverse().get(id);

BiMap的各種實現

值實現 鍵實現 對應的BiMap實現
HashMap HashMap
ImmutableMap ImmutableMap
EnumMap EnumMap

注:Maps類中還有一些諸如synchronizedBiMap的BiMap工具方法.

Table

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

通常來說,當你想使用多個鍵做索引的時候,你可能會用類似Map<FirstName, Map<LastName, Person>>的實現,這種方式很醜陋,使用上也不友好。Guava為此提供了新集合型別Table,它有兩個支援所有型別的鍵:”行”和”列”。Table提供多種檢視,以便你從各種角度使用它:

  • rowMap():用Map<R, Map<C, V>>表現Table<R, C, V>。同樣的, rowKeySet()返回”行”的集合Set<R>。
  • row(r) :用Map<C, V>返回給定”行”的所有列,對這個map進行的寫操作也將寫入Table中。
  • cellSet():用元素型別為Table.Cell<R, C, V>的Set表現Table<R, C, V>。Cell類似於Map.Entry,但它是用行和列兩個鍵區分的。

Table有如下幾種實現:

  • HashBasedTable:本質上用HashMap<R, HashMap<C, V>>實現;
  • TreeBasedTable:本質上用TreeMap<R, TreeMap<C,V>>實現;
  • ImmutableTable:本質上用ImmutableMap<R, ImmutableMap<C, V>>實現;注:ImmutableTable對稀疏或密集的資料集都有優化。
  • ArrayTable:要求在構造時就指定行和列的大小,本質上由一個二維陣列實現,以提升訪問速度和密集Table的記憶體利用率。ArrayTable與其他Table的工作原理有點不同,請參見Javadoc瞭解詳情。

ClassToInstanceMap

ClassToInstanceMap是一種特殊的Map:它的鍵是型別,而值是符合鍵所指型別的物件。

ClassToInstanceMap有唯一的泛型引數,通常稱為B,代表Map支援的所有型別的上界。例如:

ClassToInstanceMap<Number> numberDefaults=MutableClassToInstanceMap.create();
numberDefaults.putInstance(Integer.class, Integer.valueOf(0));

從技術上講,ClassToInstanceMap<B>實現了Map<Class<? extends B>, B>——或者換句話說,是一個對映B的子型別到對應例項的Map。這讓ClassToInstanceMap包含的泛型宣告有點令人困惑,但請記住B始終是Map所支援型別的上界——通常B就是Object。

RangeSet

RangeSet描述了一組不相連的、非空的區間。當把一個區間新增到可變的RangeSet時,所有相連的區間會被合併,空區間會被忽略。例如:

RangeSet<Integer> rangeSet = TreeRangeSet.create();
rangeSet.add(Range.closed(1, 10)); // {[1,10]}
rangeSet.add(Range.closedOpen(11, 15));//不相連區間:{[1,10], [11,15)}
rangeSet.add(Range.closedOpen(15, 20)); //相連區間; {[1,10], [11,20)}
rangeSet.add(Range.openClosed(0, 0)); //空區間; {[1,10], [11,20)}
rangeSet.remove(Range.open(5, 10)); //分割[1, 10]; {[1,5], [10,10], [11,20)}

請注意,要合併Range.closed(1, 10)和Range.closedOpen(11, 15)這樣的區間,你需要首先用Range.canonical(DiscreteDomain)對區間進行預處理,例如DiscreteDomain.integers()。

注:RangeSet不支援GWT,也不支援JDK5和更早版本;因為,RangeSet需要充分利用JDK6中NavigableMap的特性。

RangeSet的檢視

RangeSet的實現支援非常廣泛的檢視:

  • complement():返回RangeSet的補集檢視。complement也是RangeSet型別,包含了不相連的、非空的區間。
  • subRangeSet(Range<C>):返回RangeSet與給定Range的交集檢視。這擴充套件了傳統排序集合中的headSet、subSet和tailSet操作。
  • asRanges():用Set<Range<C>>表現RangeSet,這樣可以遍歷其中的Range。
  • asSet(DiscreteDomain<C>)(僅ImmutableRangeSet支援):用ImmutableSortedSet<C>表現RangeSet,以區間中所有元素的形式而不是區間本身的形式檢視。(這個操作不支援DiscreteDomain 和RangeSet都沒有上邊界,或都沒有下邊界的情況)

RangeSet的查詢方法

為了方便操作,RangeSet直接提供了若干查詢方法,其中最突出的有:

  • contains(C):RangeSet最基本的操作,判斷RangeSet中是否有任何區間包含給定元素。
  • rangeContaining(C):返回包含給定元素的區間;若沒有這樣的區間,則返回null。
  • encloses(Range<C>):簡單明瞭,判斷RangeSet中是否有任何區間包括給定區間。
  • span():返回包括RangeSet中所有區間的最小區間。

RangeMap

RangeMap描述了”不相交的、非空的區間”到特定值的對映。和RangeSet不同,RangeMap不會合並相鄰的對映,即便相鄰的區間對映到相同的值。例如:

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的檢視

RangeMap提供兩個檢視:

  • asMapOfRanges():用Map<Range<K>, V>表現RangeMap。這可以用來遍歷RangeMap。
  • subRangeMap(Range<K>):用RangeMap型別返回RangeMap與給定Range的交集檢視。這擴充套件了傳統的headMap、subMap和tailMap操作。