Guava引入了一些非常有用但JDK中没有的集合类型。这些新的集合类型并不是硬塞进JDK的集合框架中的,它们配合的非常好。

Multiset(多重集合)

统计一篇文档中某个单词出现了多少次,传统的Java做法是这样:

  1. Map<String, Integer> counts = new HashMap<String, Integer>();
  2. for(String word : words) {
  3. Integer count = counts.get(word);
  4. if (count == null) {
  5. counts.put(word, 1);
  6. } else {
  7. counts.put(word, count + 1);
  8. }
  9. }

这种做法非常笨拙,容易导致错误,并且无法收集一些有用的统计信息,例如所有单词的总数。我们有更好的办法。

Guava提供了一个新的集合类型,Multiset,支持添加多个元素。维基百科对multiset的数学定义如下:一般集合的概念是允许相同的元素出现多次……而multiset,类似于集合但与元组相反,是顺序无关的:{a, a, b}与{a, b, a}这两个multiset是相等的。

像下面这两种类型:

  • 类似于无序的ArrayList<E>
  • 类似于包含元素与计数的Map<E, Integer>

Guava的Multiset的API相当于把二者结合起来了:

  • 当成一般的集合来看,Multiset看起来更像是无序的ArrayList
    • 调用add(E)向集合中添加一个元素。
    • iterator()方法迭代集合中的每个元素。
    • size()方法返回集合大小。
  • 其他查询操作,包括特有的性能,类似于Map<E, Integer>
    • count(Object)返回与指定Object相关联的计数,对于HashMultiset,该操作时间复杂度为O(1),对于TreeMultiset,时间复杂度是O(log n)。
    • entrySet()方法返回类似于map的entrySet的Set<Multiset.Entry<E>>
    • elementSet()方法返回类似于map的keySet的无重复元素的Set<E>
    • Multiset实现的内存占用是无重复元素个数的线性表

另外,MultisetCollection接口是完全一致的,除了极少数的情况(之前的JDK也有类似情况),例如,TreeMultiset,类似于TreeSet,使用比较器来实现相等的判断而不是使用Object.equals()。特别的,Multiset.addAll(Collection)会在Collection中每个元素每次出现都会添加一次,而不像上面那样需要手动遍历Map

方法 描述
count(E) 为添加到此Multiset中的元素计数
elementSet() 将Multiset作为Set查看无重复的元素
entrySet() 类似于Map.entrySet(),返回一个Set>,其中的entries有getElement()和getCount()方法
add(E) 将元素添加到Multiset中
remove(E, int) 删除指定计数的指定元素
setCount(E, int) 将指定元素的计数设置为指定的非负数
size() 返回Multiset的大小

MultiSet不是Map

注意Multiset<E>不是Map<E, Integer>,尽管它是Multiset的一部分实现。Multiset是Collection类型,只是它兼容了Map的一些功能,以下是一些不同之处:

  • Multiset元素的计数只能为正数,不可能负数,而且计数为0的元素也不存在Multiset中,它们不会出现在entrySet()elementSet()的视图中。
  • Multiset.size()方法返回的是集合的大小,表示的是所有集合元素的个数(包括重复元素),如果想要获取不重复元素的大小,使用elementSet().size(),因此,add(E)会让Multiset.size()增加1。
  • Multiset.iterator()方法会迭代所有元素,迭代器的次数等于Multiset.size()
  • multiset<E>支持添加、删除元素,或直接给指定元素设置计数值,setCount(E, 0)相当于删除所有指定的元素。
  • 对不存在于Multiset中元素调用Multiset.count(elem)始终返回0。

实现

Guava有多个Multiset实现,个数大概与JDK的Map实现的个数差不多:

Map 对应的Multiset 支持null
HashMap HashMultiset Yes
TreeMap TreeMultiset Yes(如果Comparator支持)
LinkedHashMap LinkedHashMultiset Yes
ConcurrentHashMap ConcurrentHashMultiset No
ImmutableMap ImmutableMultiset No

SortedMultiset

SortedMultiset是在Multiset接口基础上的一个变体,它支持获取指定范围的子Multiset。例如,想知道100ms内网站的访问率,就要知道100ms内访问量(latencies.subMultiset(0, BoundType.CLOSED, 100, BoundType.OPEN).size())与总访问量(latencies.size())之比。

TreeMultiset实现了SortedMultiset接口,在写本文档之时,ImmutableSortedMultiset正在进行GWT兼容性的测试。

MultiMap(多重Map)

任何一个有经验的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>>的视图。最重要的是,不存在一个key映射到一个空集合:一个key映射到至少一个值,否则它在MultiMap中就不存在。

很少会直接使用MultiMap接口,更经常的是使用ListMultiMap(映射key到List)或SetMultimap(映射key到Set)。

修改

Multimap.get(key)返回的是与此key关联的所有值的视图,即使它们当前不存在。对于ListMultiMap返回的是List,对于SetMultiMap,返回的是Set

对相关的MultiMap修改操作,例如:

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

其他的一些修改操作如下:

方法签名 描述 等价操作
put(K, V) 添加一个键值对 multimap.get(K).add(V)
putAll(K, Iterable) 添加指定Iterable中的所有值并将其映射到K Iterables.addAll(multimap.get(K), V)
remove(K, V) 删除一个键值对映射,如果multimap改变了返回true multimap.get(K).remove(V)
removeAll(K) 删除并返回指定key映射的值的集合,返回的集合是可变或不可变的,但是修改这个集合不会影响到这个multimap multimap.get(K).clear()
replaceValues(K, Iterable) 替换该key关联的值为指定的迭代器  multimap.get(K).clear(); Iterables.addAll(multimap.get(K), V);

视图

Multimap提供了几种强大的视图:

  • asMapMultimap<K, V>作为Map<K, Collection<V>>视图,返回的Map支持删除操作,并会将改变写回,但不支持put()putAll()操作。另外,如果你需要不存在的key返回null而不是可写的空集合,使用asMap().get(K)(可以将其强转为需要的集合SetMultimapSetListMultimapList,但是类型系统不允许ListMultimap<K, V>返回Map<K, List<V>>)。
  • entries返回Multimap中的Collection<Map.Entry<K, V>>视图。
  • keySet返回Multimap中的不重复的key的Set集合。
  • keysMultimap中的key作为MultiSet视图返回,其中的元素可被删除,但不能添加,修改会被写回。
  • valuesMultimap中的值展平后返回Collection<V>,所有的值将会在一个集合中,类似于Iterables.concat(multimap.asMap().values()),但是返回的是一个完整的Collection

Multimap不是Map

Multimap<K, V>不是Map<K, Collection<V>>,尽管Multimap是使用这种方式实现的,以下是它们的差异:

  • Multimap.get(key)返回的始终是非null但可能是空的集合,这并不是说multimap的key映射的值占用了内存,而是返回的集合视图允许你添加关联。
  • 如果你更喜欢类似Map的操作(如果key不存在则返回null),使用asMap()视图获取Map<K, Collection<V>>(或从ListMultimap获取一个Map<K, List<V>>,使用静态的Multimaps.asMap()方法,SetMultimapSortedSetMultimap也有类似的方法)。
  • Multimap.containsKey(key)当且仅当存在元素与该key关联时才返回true。类似的,如果一个key之前关联了一个或多个值,但后来从multimap中删除了,该方法将返回false。
  • Multimap.entries()返回Multimap中的所有key的映射,如果想要key的集合,使用asMap().entrySet()
  • Multimap.size()返回整个multimap的大小,并不是不重复key的个数,使用Multimap.keySet().size()来获取无重复key的个数。

实现

Multimap提供了多种实现,在大多数情况下可以用来替代Map<K, Collection<V>>

实现 key的行为类似… value的行为类似…
ArrayListMultimap HashMap ArrayList
HashMultimap HashMap HashSet
LinkedListMultimap[^1] LinkedHashMap LinkedList
LinkedHashMultimap[^2] LinkedHashMap LinkedHashSet
TreeMultimap TreeMap TreeSet
ImmutableListMultimap ImmutableMap ImmutableList
ImmutableSetMultimap ImmutableMap ImmutableSet

除了不可变的实现,其他都支持null key 和null value。

[^1] LinkedListMultimap.entries()为无重复的key value保留了迭代顺序。

[^2] LinkedHashMultimap同时保留了entry的插入顺序,key的插入顺序以及每个key的value的顺序。

注意,以上并不是每个都是用Map<K, Collection<V>>实现的(一部分Multimap为了最小化资源占用使用了自定义的哈希表)。

BiMap

传统的键值双向映射是维护两个Map并保持它们的同步,这样很容易导致bug的出现,并且当value已经在map中的时候会让人感到困惑,例如:

  1. Map<String, Integer> nameToId = Maps.newHashMap();
  2. Map<Integer, String> idToName = Maps.newHashMap();
  3. nameToId.put("Bob", 42);
  4. idToName.put(42, "Bob");
  5. //如果Bob或42已经存在Map中该怎么办?
  6. //如果忘记同步了会出现奇怪的bug...

BiMap<K, V>是一个Map<K, V>,它能够:

  • 允许使用inverse()反转查看BiMap<V, K>
  • 保证values的惟一性,将values作为一个Set集合

BiMap.put(key, value)会抛出IllegalArgumentException如果映射key到一个已存在的value。如果要删除与此value关联的key,使用BiMap.forcePut(key, value)

  1. BiMap<String, Integer> userId = HashBiMap.create();
  2. ...
  3. String userForId = userId.inverse().get(id);

实现

key-value映射实现 value-key 映射实现 相关BiMap
HashMap HashMap HashBiMap
ImmutableMap ImmutableMap ImmutableBiMap
EnumMap EnumMap EnumBiMap
EnumMap HashMap EnumHashBiMap

注意:BiMap类似SyncronizedBiMap的工具在Maps类中。

Table

  1. Table<Vertex, Vertex, Double> weightedGraph = HashBasedTable.create();
  2. weightedGraph.put(v1, v2, 4);
  3. weightedGraph.put(v1, v3, 20);
  4. weightedGraph.put(v2, v3, 5);
  5. weightedGraph.row(v1); //返回一个映射(v2->4, v3->20)
  6. weightedGraph.column(v3); //返回一个映射(v1->20, v2->5)

当想要同时以多个key索引时,会使用到Map<FirstName, Map<LastName, Person>>这种丑陋的结构。Guava提供了一个新的集合类型 - Table,适用于这种基于“行列”的情景。它有一系列的视图可供使用:

  • rowMap(),将Table<R, C, V>作为Map<R, Map<C, V>>视图查看,类似的,rowKeySet()返回一个Set<R>
  • row(r)返回一个非null的Map<C, V>,对这个Map的操作将会影响到关联的Table
  • 类似的基于列的方法:columnMap(), columnKeySet(), column(c)(基于列的访问效率要低于基于行的访问)
  • cellSet()Table.Cell<R, C, V>的集合作为Table的视图返回

以下是Table的实现:

  • HashBasedTable,由HashMap<R, HashMap<C, V>>实现
  • TreeBasedTable,由TreeMap<R, TreeMap<C, V>>实现
  • ImmutableTable,由ImmutableMap<R, ImmutableMap<C, V>>ImmutableTable是为稀疏和密集数据集优化的实现)
  • ArrayTable,全部的行列需要在构造的时候指定,当该Table是密集型的时候为了内存和速度的效率,它由二维数组实现的。ArrayTable的实现与其他的实现有些不同,具体参见Javadoc。

ClassToInstanceMap

有些时候,Map的key不全是一致的类型:它们是某一个特定的类型(types),如果希望映射到它们具体的类型,Guava为此提供了ClassToInstanceMap

为了继承Map接口,为了避免不必要的类型转换和强制类型安全,ClassToInstanceMap提供了T getInstance(Class<T>)T putInstance(Class<T>, T)方法。

ClassToInstanceMap只有一个类型参数,命名为B,表示该map管理的类型的基类,例如:

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

ClassToInstanceMap<B>实现了Map<Class<? extends B>, B>,或者这么说,它是B的子类的类型到B的实例的映射。这会让ClassToInstanceMap中的泛型更易于理解,只要记住一点:B是Map中的基类类型 - 通常,B就是一个Object

Guava提供了两个有用的实现:MutableClassToInstanceMapImmutableClassToINstanceMap

注意:类似其他的Map<Class, Object>ClassToInstanceMap可能包含基本数据类型,而且基本数据类型和它的包装类型可能映射到不同的值。

RangeSet

注:以下用范围集来表示数学上的集合的概念,以免与Java的集合混淆。

RangeSet描述了一组断开的、非空的范围集(并集)。当把一个范围添加到一个范围集的时候,所有可连接的范围集(交集)都会合并为一个范围,空集将被忽略。例如:

  1. RangeSet<Integer> rangeSet = TreeRangeSet.create();
  2. rangeSet.add(Range.closed(1, 10)); //{[1, 10]}
  3. rangeSet.add(Range.closedOpen(11, 15)); //断开的范围集,{[1, 10], [11, 15)}
  4. rangeSet.add(Range.closedOpen(15, 20)); //可连接的范围(有交集),{[1, 10], [11, 20)}
  5. rangeSet.add(Range.openClosed(0, 0)); //空集,{[1, 10], [11, 20)}
  6. rangeSetremove(Range.open(5, 10)); //[1, 10]区间被分隔了,{[1, 5], [10, 10], [11, 20]}

注意合并像Range.closed(1, 10)Range.closedOpen(11, 15)的范围集,必须先使用Range.canonical(DiscreteDomain)预先处理范围,例如,DiscreteDomain.withIntegers()

注意:RangeSet不支持GWT,也不支持JDK1.5,因为它需要JDK1.6中的NavigableMap的支持。

视图

//TODO 未完成

RangeSet实现支持很多范围的视图,包括:

  • complement()
  • sunRangeSet(Range<C>):返回该范围集和指定的范围的交集
  • asRanges()
  • asSet(DiscreteDomain<C>)

查询

为了在视图上操作,RangeSet直接提供了几个查询操作,常用的有:

  • contains(C)RangeSet最基础的操作,查询该RangeSet中的Range是否包含指定的元素。
  • rangeContaining(C):返回包含该元素的Range,如果不包含该元素,返回null。
  • encloses(Range<C>):测试RangeSet中的Range是否包含指定的Range
  • span()://TODO

RangeMap

RangeMap是一组范围集到值的映射,不像RangeSetRangeMap从来不会合并相邻的映射,即使相邻的范围集映射到相同的值。例如:

  1. RangeMap<Integer, String> rangeMap = TreeRangeMap.create();
  2. rangeMap.put(Range.closed(1, 10), "foo"); //{[1, 10] -> "foo"}
  3. rangeMap.put(Range.open(3, 6), "bar"); //{[1, 3] - > "foo", (3, 6) -> "bar", [6, 10] -> "foo"}
  4. rangeMap.put(Range.open(10, 20), "foo"); //{[1, 3] - > "foo", (3, 6) -> "bar", [6, 10] -> "foo", (10, 20) -> "foo"}
  5. rangeMap.remove(Range.closed(5, 11)); //{[1, 3] - > "foo", (3, 5) -> "bar", [11, 20) -> "foo"}

视图

RangeMap提供了两种视图:

  • asMapOfRanges():将RangeMap作为Map<Range<K>, V>视图返回,支持迭代该RangeMap
  • subRangeMap(Range<K>),返回该RangeMap与指定Range的交集视图,传统的headMap, subMap, tailMap操作就是建立在此基础上。