鍍金池/ 教程/ Java/ 新集合類型
不可變集合
排序: Guava 強(qiáng)大的”流暢風(fēng)格比較器”
強(qiáng)大的集合工具類:java.util.Collections 中未包含的集合工具
新集合類型
常見 Object 方法
I/O
前置條件
字符串處理:分割,連接,填充
散列
原生類型
數(shù)學(xué)運(yùn)算
使用和避免 null
Throwables:簡(jiǎn)化異常和錯(cuò)誤的傳播與檢查
google Guava 包的 ListenableFuture 解析
事件總線
緩存
函數(shù)式編程
區(qū)間
集合擴(kuò)展工具類
Google-Guava Concurrent 包里的 Service 框架淺析
google Guava 包的 reflection 解析

新集合類型

Guava 引入了很多 JDK 沒有的、但我們發(fā)現(xiàn)明顯有用的新集合類型。這些新類型是為了和 JDK 集合框架共存,而沒有往 JDK 集合抽象中硬塞其他概念。作為一般規(guī)則,Guava 集合非常精準(zhǔn)地遵循了 JDK 接口契約。

Multiset

統(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:

  • 沒有元素順序限制的 ArrayList
  • Map<E, Integer>,鍵為元素,值為計(jì)數(shù)

Guava 的 Multiset API 也結(jié)合考慮了這兩種方式:
當(dāng)把 Multiset 看成普通的 Collection 時(shí),它表現(xiàn)得就像無序的 ArrayList:

  • add(E)添加單個(gè)給定元素
  • iterator()返回一個(gè)迭代器,包含 Multiset 的所有元素(包括重復(fù)的元素)
  • size()返回所有元素的總個(gè)數(shù)(包括重復(fù)的元素)

當(dāng)把 Multiset 看作 Map<E, Integer>時(shí),它也提供了符合性能期望的查詢操作:

  • count(Object)返回給定元素的計(jì)數(shù)。HashMultiset.count 的復(fù)雜度為 O(1),TreeMultiset.count 的復(fù)雜度為 O(log n)。
  • entrySet()返回 Set<Multiset.Entry>,和 Map 的 entrySet 類似。
  • elementSet()返回所有不重復(fù)元素的 Set,和 Map 的 keySet()類似。
  • 所有 Multiset 實(shí)現(xiàn)的內(nèi)存消耗隨著不重復(fù)元素的個(gè)數(shù)線性增長(zhǎng)。

值得注意的是,除了極少數(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ù)的元素)

Multiset 不是 Map

請(qǐng)注意,Multiset不是 Map<E, Integer>,雖然 Map 可能是某些 Multiset 實(shí)現(xiàn)的一部分。準(zhǔn)確來說 Multiset 是一種 Collection 類型,并履行了 Collection 接口相關(guān)的契約。關(guān)于 Multiset 和 Map 的顯著區(qū)別還包括:

  • Multiset 中的元素計(jì)數(shù)只能是正數(shù)。任何元素的計(jì)數(shù)都不能為負(fù),也不能是 0。elementSet()和 entrySet()視圖中也不會(huì)有這樣的元素。
  • multiset.size()返回集合的大小,等同于所有元素計(jì)數(shù)的總和。對(duì)于不重復(fù)元素的個(gè)數(shù),應(yīng)使用 elementSet().size()方法。(因此,add(E)把 multiset.size()增加 1)
  • multiset.iterator()會(huì)迭代重復(fù)元素,因此迭代長(zhǎng)度等于 multiset.size()。
  • Multiset 支持直接增加、減少或設(shè)置元素的計(jì)數(shù)。setCount(elem, 0)等同于移除所有 elem。
  • 對(duì) multiset 中沒有的元素,multiset.count(elem)始終返回 0。

Multiset 的各種實(shí)現(xiàn)

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

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 的兼容性。

Multimap

每個(gè)有經(jīng)驗(yàn)的 Java 程序員都在某處實(shí)現(xiàn)過 Map<K, List>或 Map<K, Set>,并且要忍受這個(gè)結(jié)構(gòu)的笨拙。例如,Map<K, Set>通常用來表示非標(biāo)定有向圖。Guava 的 Multimap 可以很容易地把一個(gè)鍵映射到多個(gè)值。換句話說,Multimap 是把鍵映射到任意多個(gè)值的一般方式。

可以用兩種方式思考 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>,讓你可以按另一種方式看待 Multimap。重要的是,不會(huì)有任何鍵映射到空集合:一個(gè)鍵要么至少到一個(gè)值,要么根本就不在 Multimap 中。

很少會(huì)直接使用 Multimap 接口,更多時(shí)候你會(huì)用 ListMultimap 或 SetMultimap 接口,它們分別把鍵映射到 List 或 Set。

修改 Multimap

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

Multimap 還支持若干強(qiáng)大的視圖:

Multimap 不是 Map

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

  • Multimap.get(key)總是返回非 null、但是可能空的集合。這并不意味著 Multimap 為相應(yīng)的鍵花費(fèi)內(nèi)存創(chuàng)建了集合,而只是提供一個(gè)集合視圖方便你為鍵增加映射值——譯者注:如果有這樣的鍵,返回的集合只是包裝了 Multimap 中已有的集合;如果沒有這樣的鍵,返回的空集合也只是持有 Multimap 引用的棧對(duì)象,讓你可以用來操作底層的 Multimap。因此,返回的集合不會(huì)占據(jù)太多內(nèi)存,數(shù)據(jù)實(shí)際上還是存放在 Multimap 中。
  • 如果你更喜歡像 Map 那樣,為 Multimap 中沒有的鍵返回 null,請(qǐng)使用 asMap()視圖獲取一個(gè) Map<K, Collection>。(或者用靜態(tài)方法 Multimaps.asMap()為 ListMultimap 返回一個(gè) Map<K, List>。對(duì)于 SetMultimap 和 SortedSetMultimap,也有類似的靜態(tài)方法存在)
  • 當(dāng)且僅當(dāng)有值映射到鍵時(shí),Multimap.containsKey(key)才會(huì)返回 true。尤其需要注意的是,如果鍵 k 之前映射過一個(gè)或多個(gè)值,但它們都被移除后,Multimap.containsKey(key)會(huì)返回 false。
  • Multimap.entries()返回 Multimap 中所有”鍵-單個(gè)值映射”——包括重復(fù)鍵。如果你想要得到所有”鍵-值集合映射”,請(qǐng)使用 asMap().entrySet()。
  • Multimap.size()返回所有”鍵-單個(gè)值映射”的個(gè)數(shù),而非不同鍵的個(gè)數(shù)。要得到不同鍵的個(gè)數(shù),請(qǐng)改用 Multimap.keySet().size()。

Multimap 的各種實(shí)現(xiàn)

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>來實(shí)現(xiàn)(特別是,一些 Multimap 實(shí)現(xiàn)用了自定義的 hashTable,以最小化開銷)

如果你想要更大的定制化,請(qǐng)用 Multimaps.newMultimap(Map, Supplier)listset 版本,使用自定義的 Collection、List 或 Set 實(shí)現(xiàn) Multimap。

BiMap

傳統(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:

  • 可以用 inverse()反轉(zhuǎn) BiMap<K, V>的鍵值映射
  • 保證值是唯一的,因此 values()返回 Set 而不是普通的 Collection

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

BiMap 的各種實(shí)現(xiàn)

值實(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


    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):

  • HashBasedTable:本質(zhì)上用 HashMap<R, HashMap<C, V>>實(shí)現(xiàn);
  • TreeBasedTable:本質(zhì)上用 TreeMap<R, TreeMap<C,V>>實(shí)現(xiàn);
  • ImmutableTable:本質(zhì)上用 ImmutableMap<R, ImmutableMap<C, V>>實(shí)現(xiàn);注:ImmutableTable 對(duì)稀疏或密集的數(shù)據(jù)集都有優(yōu)化。
  • ArrayTable:要求在構(gòu)造時(shí)就指定行和列的大小,本質(zhì)上由一個(gè)二維數(shù)組實(shí)現(xiàn),以提升訪問速度和密集 Table 的內(nèi)存利用率。ArrayTable 與其他 Table 的工作原理有點(diǎn)不同,請(qǐng)參見 Javadoc 了解詳情。

ClassToInstanceMap

ClassToInstanceMap 是一種特殊的 Map:它的鍵是類型,而值是符合鍵所指類型的對(duì)象。

為了擴(kuò)展 Map 接口,ClassToInstanceMap 額外聲明了兩個(gè)方法:T getInstance(Class)T putInstance(Class, T),從而避免強(qiáng)制類型轉(zhuǎn)換,同時(shí)保證了類型安全。

ClassToInstanceMap 有唯一的泛型參數(shù),通常稱為 B,代表 Map 支持的所有類型的上界。例如:


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

從技術(shù)上講,從技術(shù)上講,ClassToInstanceMap<B&gt 實(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):MutableClassToInstanceMapImmutableClassToInstanceMap

RangeSet

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

RangeSet 的實(shí)現(xiàn)支持非常廣泛的視圖:

  • complement():返回 RangeSet 的補(bǔ)集視圖。complement 也是 RangeSet 類型,包含了不相連的、非空的區(qū)間。
  • subRangeSet(Range):返回 RangeSet 與給定 Range 的交集視圖。這擴(kuò)展了傳統(tǒng)排序集合中的 headSet、subSet 和 tailSet 操作。
  • asRanges():用 Set<Range>表現(xiàn) RangeSet,這樣可以遍歷其中的 Range。
  • asSet(DiscreteDomain)(僅 ImmutableRangeSet 支持):用 ImmutableSortedSet表現(xiàn) RangeSet,以區(qū)間中所有元素的形式而不是區(qū)間本身的形式查看。(這個(gè)操作不支持 DiscreteDomain 和 RangeSet 都沒有上邊界,或都沒有下邊界的情況)

RangeSet 的查詢方法

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

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

RangeMap

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

RangeMap 提供兩個(gè)視圖:

  • asMapOfRanges():用 Map<Range, V>表現(xiàn) RangeMap。這可以用來遍歷 RangeMap。
  • subRangeMap(Range):用 RangeMap 類型返回 RangeMap 與給定 Range 的交集視圖。這擴(kuò)展了傳統(tǒng)的 headMap、subMap 和 tailMap 操作。