(Guava 译文系列)新集合类型
新集合类型
Guava 引入了许多 JDK 未包含但我们却广泛使用的的新集合类型。他们全都设计为可以和 JDK 的集合框架友好的共存, 而不是硬塞入 JDK 的集合类抽象中。
一般来说,Guava 的集合会精确地按照 JDK interface 所定义的契约来实现。
Multiset
按照传统的 Java 习惯,计算单词在文档中出现的次数通常会按照如下方式:
1 | Map<String, Integer> counts = new HashMap<String, Integer>(); |
上述方案不仅尴尬,容易出错,而且不支持收集更多的有用的统计信息,比如单词总数等。我们可以做得更好。
Guava 提供了新的集合类型,Multiset,支持多次添加同一个元素。维基百科定义
Multiset 为在数学上 “集合概念的一种推广,其中成员可以出现多次...在
multisets 中,与 set 类似,与 tuple 相反,其与成员的顺序无关:{a, a, b}
和 {a, b, a} 是等同的 multiset”。
有两种主流的方式来理解:
- multiset
就像一个没有顺序限制的
ArrayList<E>:顺序无关紧要。 - 这就像是一个
Map<E, Integer>,包括元素和计数。
API结合了上述对Multiset的两种理解方式,如下:
- 当他被视为一个普通的
Collection,Multiset表现的行为更像是一个未排序的ArrayList:- 调用
add(E)将给定的元素添加单个引用。 - Multiset 的
iterator()遍历每一个元素的每一个引用 - Multiset 的
size()为每一个元素的每一个引用的总和
- 调用
- 附加的查询操作及其性能特性,与你对
Map<E, Integer>的期望一致。count(Object)返回与该元素相关联的计数。对HashMultiset,计数的复杂度是O(1),对TreeMultiset,计数的复杂度O(log n)。entrySet()返回一个Set<Multiset.Entry<E>>,与Map的 entrySet 类似。elementSet()返回一个对 multiset 元素去重的Set<E>,就像Map的keySet()。Multiset实现的内存消耗与不同元素的数量线性相关。
值得注意的是,Multiset 的契约与 Collection
interface 完全一致,除非在极少数的情况下存在 JDK
自身的先例。特别的,TreeMultiset 像 TreeSet
一样,
使用比较来判断相等而不是Object.equals.另外,Multiset.addAll(Collection)方法,会在Collection中的元素每次出现时,对其引用加一,这样会比先前采用循环添加Map的方式方便很多。
| Method | Description |
|---|---|
count(E) |
计算被加入到 multiset 中的元素出现的次数 |
elementSet() |
以Set<E>的形式查看去重了的
Multiset<E>元素 |
entrySet() |
类似 Map.entrySet(), 返回一个
Set<Multiset.Entry<E>>, 包含支持
getElement() and getCount() 的 entry. |
add(E, int) |
给指定的元素增加指定的引用次数 |
remove(E, int) |
给指定的元素减少指定的引用次数 |
setCount(E, int) |
给指定的元素设置指定的非负引用次数 |
size() |
返回Multiset中的所有元素的所有引用次数的总和 |
Multiset 不是 Map
请注意 Multiset<E> 不是
Map<E, Integer>, 即便是它可能属于
Multiset 实现的一部分. Multiset 是一个真正的
Collection 类型, 满足其所有相关的契约义务.
其他值得注意的不同点包括:
- 一个
Multiset<E>只能包含记录正数次数的元素。没有元素可以拥有负数计数,且计数为0的值被认为不包含在 multiset 中。他们不会在elementSet()或entrySet()中出现。 multiset.size()返回集合的尺寸,等于所有元素的计数之和。如果想要了解去重后的元素的数量,使用elementSet().size()。(所以,例如add(E)会对multiset.size()增加一。)multiset.iterator()对每个元素的每个引用进行遍历,所以该迭代器的长度等于multiset.size()。Multiset<E>支持添加元素,删除元素,或直接设定元素的计数值。setCount(elem, 0)相当于删除该元素的所有引用。multiset.count(elem)对不包含在 multiset 中的元素总会返回0。
实现类
Guava 提供了许多Multiset的实现,大致与 JDK
的实现相对应。
| Map | Corresponding Multiset | Supports null elements |
|---|---|---|
HashMap |
HashMultiset |
Yes |
TreeMap |
TreeMultiset |
Yes |
LinkedHashMap |
LinkedHashMultiset |
Yes |
ConcurrentHashMap |
ConcurrentHashMultiset |
No |
ImmutableMap |
ImmutableMultiset |
No |
SortedMultiset
SortedMultiset是Multiset接口的一种变体,它能够支持按照特定范围来高效的取出
sub-multisets。例如你可以使用latencies.subMultiset(0, BoundType.CLOSED, 100, BoundType.OPEN).size()来测算你的网站中有多少点击延迟小于
100ms
并且与latencies.size()相比较来测算与点击总量的占比。
TreeMultiset 实现了 SortedMultiset
接口。在本文撰写时,ImmutableSortedMultiset对 GWT
的兼容性测试仍在进行中。
Multimap
每一个有经验的 Java
程序员,在某些时刻,都尝试实现并处理过一种令人尴尬的结构:
Map<K, List<V>> 或
Map<K, Set<V>>。例如
Map<K, Set<V>>
是一种典型的表示未标记有向图的方式。Guava 的 Multimap框架让处理一个
key 与多个 value 之间的映射变得简单。Multimap
是一种通用的关联单个 key 与任意多个 value 的方法。
理解 Multimap 的概念有两种办法:看作是单个 key 对单个 value 的映射的集合:
1 | a -> 1 |
或者作为唯一 key 到 value 集合的映射:
1 | a -> [1, 2, 4] |
通常,Multimap
接口根据第一种视角来理解是最好的,但他也允许你按照另一种视角来看待,即以asMap()的视角,他会返回
Map<K, Collection<V>>。重要的是一个 key
映射一个空集合的形式并不存在:一个 key 映射到至少一个
value,否则他就不会在Multimap中存在。
很少会直接使用 Multimap 接口,反之,更多情况你会使用
ListMultimap 或 SetMultimap,对应分别将 key
映射为一个List 或一个 Set。
构建
创建一个 Multimap 最直接的方式就是使用 MultimapBuilder,它允许你配置
key 和 value 以怎样的形式展现。比如:
1 | // creates a ListMultimap with tree keys and array list values |
你也可以选择直接在实现类上使用 create()
方法,只不过这样相比于 MultimapBuilder 有一些不妥。
修改
Multimap.get(key)
返回与指定 key 关联的所有值的视图,即使当前并没有值。对
ListMultimap 他会返回一个 List,对
SetMultimap 他会返回一个 Set。
修改操作通过底层的 Multimap 来进行写。例如,
1 | Set<Person> aliceChildren = childrenMultimap.get(alice); |
通过底层 multimap 来写.
Other ways of modifying the multimap (more directly) include: 另外的(更直接的)修改 multimap 的方法包括:
| Signature | Description | Equivalent |
|---|---|---|
put(K, V) |
增加一个 key 和 value 的关联 | multimap.get(key).add(value) |
putAll(K, Iterable<V>) |
按顺序增加 key 与 value 的关联。 | Iterables.addAll(multimap.get(key), values) |
remove(K, V) |
删除一个 key 与
value 的关联并在 multimap 被修改后返回
true. |
multimap.get(key).remove(value) |
removeAll(K) |
删除所有与 key 相关联的 value 并将这些 value 返回. 返回的集合可能可以也可能不可以被修改, 但不论怎样对其进行修改都不会影响原 multimap. (会返回何时的集合类型。) | multimap.get(key).clear() |
replaceValues(K, Iterable<V>) |
删除所有与 key 关联的
value 并且将 key 与新的 values
关联. 返回先前与key 关联的所有值。 |
multimap.get(key).clear(); Iterables.addAll(multimap.get(key), values) |
视图
Multimap 也支持多种强大的视图.
asMap将任何Multimap<K, V>以Map<K, Collection<V>>的形式展示。返回的 map 支持remove,修改会写回原对象,但他并不支持put或putAll。重要的是,当你期望对不存在的 key 返回null而不是空的可写集合时,你可以使用asMap().get(key)。(你可以也应该将asMap.get(key)的返回值转换为适当的集合类型 -- 对SetMultimap返回Set,对ListMultimap返回List-- 然而类型系统不允许ListMultimap返回Map<K, List<V>>。)entries以Collection<Map.Entry<K, V>>形式展示Multimap的所有 entries。(对SetMultimap会返回一个Set。)keySet以的Set形式展示Multimap中所有不重复的 key。keys以Multiset的形式展示Multimap的所有 key,相同 key 的数量与对应的值的数量一致。允许从Multiset中删除元素,但不允许添加,相应的更改会被写入原对象。values()以“拍平”的Collection<V>形式展示Multimap所有的值,值都处于同一个集合中。这与Iterables.concat(multimap.asMap().values())很类似,只是返回了一个满的Collection。
Multimap 不是 Map
Multimap<K, V> 不是
Map<K, Collection<V>>,尽管上述 map 可能被用于
Multimap 的实现。其显著的差异包括:
Multimap.get(key)总是返回一个非 null,但可能为空的集合。这并不意味着 multimap 会耗费内存来与 key 做关联,相反,返回的集合是一个视图,能允许你根据需要添加与该 key 的关联。- 假如你更喜欢当 multimap 中不存在 key 时返回
null这种类似Map的行为,使用asMap()视图来获得一个Map<K,Collection<V>>,(或者使用静态方法Multimaps.asMap()来从ListMultimap中获取Map<K,List<V>>,类似的方法在SetMultimap和SortedSetMultimap亦存在) - 当且仅当对指定的 key
存在相关联的任意个元素时,
Multimap.containsKey(key)返回 true。特别的,如果一个 keyk曾经与一个或多个元素关联,但这些元素已经被移除,则Multimap.containsKey(key)返回 false。 Multimap.entries()返回Multimap中所有 key 的所有 entries。假如你想要一个 key-collection 的 entries,则使用asMap().entrySet()。Multimap.size()返回整个 multimap 中所有 entries 的数量,而不是无重复的 key 的数量。想要获得无重复 key 的数量,使用Multimap.keySet().size()。
实现类
Multimap
提供了大量的实现类。你可以在大多数情况下使用他来替代
Map<K, Collection<V>>。
| Implementation | Keys behave like... | Values behave like.. |
|---|---|---|
ArrayListMultimap |
HashMap |
ArrayList |
HashMultimap |
HashMap |
HashSet |
LinkedListMultimap
* |
LinkedHashMap``* |
LinkedList``* |
LinkedHashMultimap** |
LinkedHashMap |
LinkedHashSet |
TreeMultimap |
TreeMap |
TreeSet |
ImmutableListMultimap |
ImmutableMap |
ImmutableList |
ImmutableSetMultimap |
ImmutableMap |
ImmutableSet |
除了不可变的类以外,每一个实现类都支持 null key 和 value。
* LinkedListMultimap.entries() 在重复的 key
和 value 之间保留迭代顺序。点击链接查看详情。
** LinkedHashMultimap 保留 entries
的插入顺序,也即是 key 的插入顺序,以及与任何一个 key 关联的 value
集。
注意并不是所有列出的实现的其本质都是
Map<K, Collection<V>>!(尤其是一些
Multimap的实现采用定制化的哈希表来使得开销最小。)
假如你想要更多的定制,使用Multimaps.newMultimap(Map, Supplier<Collection>)或
list
以及 set
的变体或使用自定义集合,列表或集来实现支持 multimap。
BiMap
将 value 映射回 key 的传统方法是维护两个独立的 map 并且使他们保持同步,然而,这种方式容易出错,且当一个 value 已经存在于 map 中时,会变得非常令人困惑。例如:
1 | Map<String, Integer> nameToId = Maps.newHashMap(); |
一个 BiMap<K, V>
是一个 Map<K, V>,它能够:
假如你尝试将 key 映射至一个已经存在的 value
时,BiMap.put(key, value)会抛出IllegalArgumentException。假如你期望删除具有指定
value 的任何已经存在的 entry,可以使用 BiMap.forcePut(key, value)
1 | BiMap<String, Integer> userId = HashBiMap.create(); |
实现类
| Key-Value Map Impl | Value-Key Map Impl | Corresponding BiMap |
|---|---|---|
HashMap |
HashMap |
HashBiMap |
ImmutableMap |
ImmutableMap |
ImmutableBiMap |
EnumMap |
EnumMap |
EnumBiMap |
EnumMap |
HashMap |
EnumHashBiMap |
注意: BiMap 工具类例如
synchronizedBiMap 在 Maps
中存在.
Table
1 | Table<Vertex, Vertex, Double> weightedGraph = HashBasedTable.create(); |
通常,当你尝试同时对超过一个 key 进行索引时,你最终会选择类似
Map<FirstName, Map<LastName, Person>>
的难看而尴尬的解决方案。Guava 提供了一个新的集合类, Table,用于支持
"row" 类型和 "column" 类型的情况。Table
支持多种视图,来提供多角度的数据展示,包括:
rowMap(), 将Table<R, C, V>展示为Map<R, Map<C, V>>. 同样的,rowKeySet()返回一个Set<R>.row(r)返回一个非 nullMap<C, V>. 写入这个Map写入底层的Table.- 类似的也提供了类方法:
columnMap(),columnKeySet(), 和column(c). (基于列的查询会比基于行的效率更低一些) cellSet()的返回将Table展示为一组Table.Cell<R, C, V>.Cell就像Map.Entry, 但区分了行和列的 key。
多种 Table 的实现类如下:
HashBasedTable, 实际上底层是HashMap<R, HashMap<C,V>>。TreeBasedTable, 实际上底层是TreeMap<R, TreeMap<C,V>>。ImmutableTableArrayTable,它要求在创建时即指定完整的行和列,当表密集时通过二维数组来提升速度与内存效率。ArrayTable与其他的实现上有些许不同,查看 javadoc 来获取详情。
ClassToInstanceMap
有些时候,map 中的 key
不一定全都是同样的类型:他们是各种类型,且你想要让某种类型映射对应其类型的值。Guava
提供了 ClassToInstanceMap
来实现它。
除了扩展 Map 接口外,ClassToInstanceMap
提供了 T getInstance(Class<T>)
和 T putInstance(Class<T>, T)
方法,以此来避免在强制类型安全时不愉快的类型转换。
ClassToInstanceMap 有一个单一类型的参数,通常命名为
B,代表由 map 管理的类型上界。例如:
1 | ClassToInstanceMap<Number> numberDefaults = MutableClassToInstanceMap.create(); |
技术上讲,ClassToInstanceMap<B> 实现了
Map<Class<? extends B>, B> --
或者换句话说,一个 B 的子类映射到 B 的一个实例。这让
ClassToInstanceMap 引入泛型变得有点混乱,但只要记住
B 总是 map 的上边界 -- 通常 B 只是
Object。
Guava 提供了名为 MutableClassToInstanceMap
和 ImmutableClassToInstanceMap
的实现类。
重要:就像其他的Map<Class, Object>一样,ClassToInstanceMap
也许会包含基本类型的
entries,因此基本类型和其对应的包装类也许会映射至不同的值。
RangeSet
A RangeSet describes a set of disconnected,
nonempty ranges. When adding a range to a mutable
RangeSet, any connected ranges are merged together, and
empty ranges are ignored. For example: RangeSet
描述了一种存储 非连续,非空 范围的 set。当给不可变的
RangeSet
增加了一个范围时,任何连续的范围被合并,空范围被忽略。例如:
1 | RangeSet<Integer> rangeSet = TreeRangeSet.create(); |
注意当合并类似Range.closed(1, 10) 和
Range.closedOpen(11, 15) 时,你需要通过 Range.canonical(DiscreteDomain)
来对范围进行预处理,例如 DiscreteDomain.integers()。
NOTE: RangeSet is not supported under
GWT, nor in the JDK 1.5 backport; RangeSet requires full
use of the NavigableMap features in JDK 1.6.
注意:RangeSet 在 GWT 和 JDK 1.5
之前都不受支持,RangeSet依赖 JDK 1.6 的
NavigableMap 功能。
视图
RangeSet 的实现类支持非常广泛的范围视图,例如:
complement():RangeSet的补集视图。complement也是一个RangeSet,即包含非连续、非空的范围。subRangeSet(Range<C>): 返回RangeSet中指定Range的交叉视图。这可以概括为传统有序集合的headSet,subSet, 和tailSet视图。asRanges(): 以可迭代的Set<Range<C>>形式展示RangeSet。asSet(DiscreteDomain<C>)(ImmutableRangeSetonly): 以ImmutableSortedSet<C>的形式展示RangeSet<C>,展示其中的元素,而不是范围本身。(这个操作不支持在DiscreteDomain和RangeSet都无上界或下界的情况)
查询
除了对视图的操作以外,RangeSet
还支持多种直接查询操作,其中最突出的有:
contains(C):RangeSet最基础的操作,查询RangeSet的任意范围中是否包含给定的元素。rangeContaining(C): 返回包围了指定元素的范围,若不存在则返回null。encloses(Range<C>): 直截了当,测试RangeSet中是否存在任何Range包含了给定的范围。span(): 返回一个最小的Range,他能encloses所有RangeSet中的范围。
RangeMap
RangeMap
是一个集合类型,描述一个不相交、非空的范围与值的映射。不像RangeSet,RangeMap
从不“合并”相邻的映射, 即使相邻的映射指向相同的值。例如:
1 | RangeMap<Integer, String> rangeMap = TreeRangeMap.create(); |
视图
RangeMap 提供两种视图:
asMapOfRanges(): 以Map<Range<K>, V>的形式展示RangeMap。例如这可以用于遍历RangeMap。subRangeMap(Range<K>)将RangeMap与指定的Range相交为一个RangeMap。这可以概括为传统headMap,subMap, 和tailMap操作。