Guava中的集合类

Guava提供了若干个新的集合类型,它们很常用但是却不在JCF(Java Collections Framework)中,这组集合类型遵循JCF的API接口约定,因此可以用熟悉的方式才操作它们,只要做很少的改动就可以将遗留代码迁移到新的集合上。

每个集合类型都有各自适用的场景,谨慎明智地选择其中之一。

Guava 提供了 Multiset Multimap BiMap Table RangeSet RangeMap 多种数据结构并且提供了很多种类的实现,如:

  • Multiset 的实现包括 HashMultiset TreeMultiset LinkedHashMultiset ConcurrentHashMulitset ImmutableMulitset EnumMultiset
  • MultiMap 的实现包括 ArrayListMultimap HashMultimap LinkedListMultimap LinkedHashMultimap TreeMultimap ImmutableListMultimap ImmutableSetMultimap
  • BiMap 的实现包括 HashBiMap ImmutableBiMap EnumBiMap EnumHashBiMap
  • Table 的实现包括 HashBasedTable TreeBasedTable ImmutableTable ArrayTable

这些实现跟JDK中的集合类型一样覆盖了哈希、链表、并发实现,并且提供了JDK中没有的不可变(immutable)实现,以在不同的场景下获得最佳的性能。

注意:所有这些新的集合类都有对应的 Immutable、Synchronized、Unmodified 实现。

Multiset

Multiset 类似于 Set,支持顺序无关的相等性,并且一个元素可以出现多次,从概念上讲就是一个包(bag)数据结构。Multiset 中相同的多个元素被认为是一个元素出现多次(occurrences),次数总和由 API count 方法返回。Wikipedia 中定义 Multiset 为“在数学中,允许一个元素出现多次的通用集合(set)的概念”,比如 {a,a,b} 和 {a,b} 是不同的 Multiset 但却是相同的 set。在 Multiset 中,元素是顺序无关的,因此 {a, a, b} 和 {a, b, a} 是一样的。

Multiset 优化了 Collection 的 contains containsAll remove removeAll retainAll 方法,使得即便在错误的类型或者 null 值时也不会抛出异常。除此之外,Multiset 还提供了 add(E, int) remove(E, int) setCount(E, int) 来批量添加、删除、设置元素的出现次数。当需要所有元素的出现次数总和时可以调用size()方法。iterator() 将遍历所有元素的每次出现,它的长度等于 size 。elementSet() 提供了 Multiset 的非重复元素集合, entrySet() 方法同样返回非重复元素集合,在此基础上还提供每个元素的次数,这个集合由 Multiset.Entry 组成,每个对象包含 getElement()getCount() 方法。Multiset 提供了两个静态方法 create()create(Iterable<? extends E>) 来构建新的 Multiset 对象。

除了 TreeMultiset 用 compare 比较元素的同一性,别的实现都是用 equals 方法进行比较。TreeMultiset 是 SortedMultiset 的一种实现,支持快速选取子 Multiset 以及按大小进行遍历。

Multiset 和 ArrayList 有些类似,除了在 Multiset 中元素顺序是无关的。Multiset 和 Map<E, Integer> 也有些类似,除了 Multiset 不能包含 0 或者负数的出现次数。

需要注意的是 Multiset 中的元素出现次数一定是正整数,当为 0 时,元素将从 Multiset 中删除。如果你需要的集合能够应对 0 、负数或者范围超出整数时,可以使用 Guava 中的 AtomicLongMap 。

Multimap

Multimap 是一种类似于 Map 的数据结构,并且一个键可以关联多个值。你可以把 multimap 看做一个键对应非空集合的映射表,也可以把它看做多个键值对的扁平结构。比如:

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

是等价的。最重要的是,一个键不会对应一个空集合,即:一个键要么至少对应一个值,要么不存在于 Multimap 中。尽管在概念上看第一种方式更能够表达问题域,但是 Multimap 的 API 设计是基于第二种形式的。所以上面的 size() 方法将返回 5 而不是 3,values() 方法返回 [1, 2, 4, 3, 5] 而不是 [[1,2,4], [3], [5]] 。put(K, V) 将键值对添加到 Multimap 中,putAll(K, Iterable<V>) 将键与每个值的关联添加进来,remove(K, V) 将键值对从 Multimap 中移除, removeAll(K) 移除并返回与键关联的所有值的集合,修改这个集合将不影响 Multimap 。replaceValues(K, Iterable<V>) 替换与键关联的所有值为新的值集合,并返回原来的值集合,修改这个集合将不影响 Multimap 。

Multimap 有一个非常重要的方法: get(key) 返回与键关联的值的视图,可以通过这个视图给对应的键添加、删除值,如果一个键不在 Multimap 中将返回空集合而不是 null 值。这个视图与 Multimap 是相互影响的,修改任何一个都会同时改变另外一个。示例代码:

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

这样做的一个好处是不用在添加键值对之前先添加一个空集合,比如:

1
2
3
4
5
6
if ((aliceChildren = childrenMap.get(alice)) == null) {
  aliceChildren = new HashSet<>();
  childrenMap.put(alice, aliceChildren);
}
aliceChildren.add(bob);
aliceChildren.add(carol);

虽然一个不在 Multimap 中的键调用 get 方法时会返回空集合,但是 containsKey() 方法将返回 false,表示对应的键不在 Multimap 中。如果任何操作会导致一个键与 0 个值关联将导致键从 Multimap 中移除。如果需要在不存在时返回 null 值,可以调用 asMap().get(K) 方法。

Multimap 拥有多个视图,这些视图反应了 Multimap 的最新状态,并且与 Multimap 是相互影响,改变任何一方都会影响(write-through) Multimap 的底层。反过来 Multimap 的实际内容改变也会同时影响到视图。

  • asMap(): Map<K, Collection<V>> 将 Multimap<K, V> 当做一个 Map<K, Collection> ,与一个键相关的所有值都在 Collection 中,可以往返回的 Collection 中添加/替换/删除数据,但不能替换/删除 Collection 本身;
  • keys(): Multiset<K> 将 Multimap 中的键与出现次数当做一个 Multiset ,可以对 Multiset 进行删除但不能新增,结果将反映到 Multimap 本身;
  • keySet(): Set<K> 将 Multimap 的所有去重后的键当做一个 Set ;
  • values(): Collection<V> 返回所有值的扁平集合;
  • entries(): Collection<Entry<K,V>> 返回所有键值对的扁平集合;

以上所有视图都不是真正的创建对应的集合,而是代理到 Multimap 本身,所以才称之为视图

Multimap 有两个子接口:ListMultimapSetMultimap 。ListMultimap 中同一个键的值集合类似于 List,并保存插入顺序。SetMultimap 中的同一键的值的集合类似于 Set,值得注意的是 SetMultimap 中同一键的值不能重复,而 ListMultimap 的值可以重复。如果希望 SetMultimap 中的值按大小排序,可以选择 SortedSetMultimap 接口。更常用的是这些子接口而不是通用接口,这个跟 Collection 与 List 和 Set 的关系类似。需要注意的是以上接口只指定了值的顺序并没有具体指定键的顺序。键的顺序和值的顺序是相互独立的。对于 SetMultimap 的实现 TreeMultimap 保存了键和值的大小顺序,LinkedHashMultimap 保存了键和值的插入顺序,HashMultimap 没有保存键和值的任何顺序。

需要注意的是多种 Multimap 实现的 equalshashCode 的实现策略很可能是不同的,即便键值对都完全一致,因为对于 ListMultimap 键值对的顺序也会产生影响,所以最好不要拿它们去做比较,而子接口则做了更强的保证。除了以上标准实现外,可以通过传入指定的 Map 和 Collection 的类型给 Multimaps.newMultimap 方法族来定制自己的 Multimap 。

BiMap

BiMap 是一种键和值都是唯一的映射表(Map),BiMap 的这种限制让它能够非常高效的表示由值查找键的反向视图(inverse view),因而也被成为双向映射(Bidirectional Map)。在没有 BiMap 的情况下,要表示值到键的映射需要维护另外一个独立的 Map ,但是这样容易出错,特别是在值有重复的情况下。

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

nameToId.put("Bob", 42);
idToName.put(42, "Bob");
// what happens if "Bob" or 42 are already present?
// weird bugs can arise if we forget to keep these in sync...

BiMap<K, V> 继承自 Map<K, V>,更进一步的,通过 inverse() 得到一个反向 BiMap<V, K>,确保所有的值都是唯一的,因而 values() 返回一个 Set

BiMap.put(key, value) 会在值已经存在的情况下抛出 IllegalArgumentException,如果希望删除掉之前值对应的条目(Entry)调用 BiMap.forcePut(key, value)