Guava引入了一些非常有用但JDK中没有的集合类型。这些新的集合类型并不是硬塞进JDK的集合框架中的,它们配合的非常好。
Multiset(多重集合)
统计一篇文档中某个单词出现了多少次,传统的Java做法是这样:
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的数学定义如下:一般集合的概念是允许相同的元素出现多次……而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实现的内存占用是无重复元素个数的线性表
另外,Multiset与Collection接口是完全一致的,除了极少数的情况(之前的JDK也有类似情况),例如,TreeMultiset,类似于TreeSet,使用比较器来实现相等的判断而不是使用Object.equals()。特别的,Multiset.addAll(Collection)会在Collection中每个元素每次出现都会添加一次,而不像上面那样需要手动遍历Map。
| 方法 | 描述 |
|---|---|
| count(E) | 为添加到此Multiset中的元素计数 |
| elementSet() | 将Multiset |
| entrySet() | 类似于Map.entrySet(),返回一个Set |
| 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修改操作,例如:
Set<Person> aliceChildren = childrenMultimap.get(alice);aliceChildren.clear();aliceChildren.add(bob);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提供了几种强大的视图:
asMap把Multimap<K, V>作为Map<K, Collection<V>>视图,返回的Map支持删除操作,并会将改变写回,但不支持put()或putAll()操作。另外,如果你需要不存在的key返回null而不是可写的空集合,使用asMap().get(K)(可以将其强转为需要的集合SetMultimap的Set或ListMultimap的List,但是类型系统不允许ListMultimap<K, V>返回Map<K, List<V>>)。entries返回Multimap中的Collection<Map.Entry<K, V>>视图。keySet返回Multimap中的不重复的key的Set集合。keys将Multimap中的key作为MultiSet视图返回,其中的元素可被删除,但不能添加,修改会被写回。values将Multimap中的值展平后返回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()方法,SetMultimap和SortedSetMultimap也有类似的方法)。 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中的时候会让人感到困惑,例如:
Map<String, Integer> nameToId = Maps.newHashMap();Map<Integer, String> idToName = Maps.newHashMap();nameToId.put("Bob", 42);idToName.put(42, "Bob");//如果Bob或42已经存在Map中该怎么办?//如果忘记同步了会出现奇怪的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)。
BiMap<String, Integer> userId = HashBiMap.create();...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
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); //返回一个映射(v2->4, v3->20)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管理的类型的基类,例如:
ClassToInstanceMap<Number> numberDefaults = MutableClassToInstanceMap.create();numberDefaults.putIntance(Integer.class, Integer.valueOf(0));
ClassToInstanceMap<B>实现了Map<Class<? extends B>, B>,或者这么说,它是B的子类的类型到B的实例的映射。这会让ClassToInstanceMap中的泛型更易于理解,只要记住一点:B是Map中的基类类型 - 通常,B就是一个Object。
Guava提供了两个有用的实现:MutableClassToInstanceMap和ImmutableClassToINstanceMap。
注意:类似其他的Map<Class, Object>,ClassToInstanceMap可能包含基本数据类型,而且基本数据类型和它的包装类型可能映射到不同的值。
RangeSet
注:以下用范围集来表示数学上的集合的概念,以免与Java的集合混淆。
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.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是一组范围集到值的映射,不像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提供了两种视图:
asMapOfRanges():将RangeMap作为Map<Range<K>, V>视图返回,支持迭代该RangeMap。subRangeMap(Range<K>),返回该RangeMap与指定Range的交集视图,传统的headMap,subMap,tailMap操作就是建立在此基础上。
