(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>>
。ImmutableTable
ArrayTable
,它要求在创建时即指定完整的行和列,当表密集时通过二维数组来提升速度与内存效率。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>)
(ImmutableRangeSet
only): 以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
操作。