1 前言
JDK 提供了各种功能强大的工具类, 宛如装备齐全的军火库, 而容器就是其中一项内置的利器, 提供了包括诸多常用的数据结构, 下图对 JDK 已有容器进行了概括:
不过, 虽然 JDK 的容器类已经五花八门, 琳琅满目, 但是某些很有用的容器类 JDK 依然欠缺, 而 Guava 恰如其分地填补了这些空缺, 开发了 JDK 所欠缺的容器类, “造福大众”.
此外, 虽然引入了新的容器类, 但 Guava 实现了
JDK 的 Collection
接口, 保证 Guava 的容器类能够与 JDK
的容器类”和谐共处”, 避免不必要的”纷争”.
2 Multiset
假设你是个书店的老板, 你想统计下书店里不同书籍的存货量, 你可能写下这样的实现:
|
|
嗯, 我现在想改需求, 我想知道书店里共有多少本书? 怎么办呢? 把 counts
的
value 都加起来?
对于这样的要求, Guava 提供了一个更好的解决方案: 一个新类型容器
Multiset
, 它支持新增多个相同类型的元素并统计. 维基百科给出的关于
Multiset
的解释:
这是个成员可以出现多次的集合(Set), 也被称为背包(bag)
大名鼎鼎的《算法/Algorithm》也给出过 bag 的解释和实现.
需要注意的是, multisets
的成员是无序的, {a,a,b}
和 {a,b,a}
这两个集合在 multisets
看来是相等.
我们可以从两个角度来分析 multisets
:
multisets
就好像一个ArrayList<E>
, 只不过是无序的. 当把它当作ArrayList<E>
时:- 调用
add(E)
函数, 增加给定元素的出现次数 - 调用
iterator()
函数, 获取一个multisets
的迭代器, 用来迭代每个元素 - 调用
size()
函数, 获取所有元素出现次数之和
- 调用
multisets
就好象一个Map<E, Integer>
, 包含元素和对应的数量, 只不过数量只能为正数. 当把它当作Map<E, Integer>
的时候:- 调用
count(Object)
函数获取某个特定元素的出现次数. - 调用
entrySet()
函数返回一个Set<Multiset.Entry<E>>
, 大概类似一个Map
返回entrySet
. - 调用
elementSet
函数返回一个Set<E>
对象, 返回所有的元素(去掉重复的元素)
- 调用
2.1 Multiset 的例子
粗略介绍完 Multiset
之后, 现在就让我们用它重新实现原来的需求:
|
|
multisets
完美满足了我们的需求.
2.2 Multiset 并不是一个 Map
需要注意的是, Multiset
虽然与Map<E, Integer>
类似, 但 Multiset
并不是一个Map<E, Integer>
, 请不要混淆它们两个.
最大的差别是, Multiset
实现了 Collection
接口, 完全遵守 Collection
接口需要满足的协议, 而 Map
和 Collection
是完全不同的接口,
这点需要牢记于心. 还有其他的差别, 诸如:
Multiset<E>
出现的次数只能是正数, 没有任何元素的出现次数会是负数的, 出现次数为 0 的元素会被认为不存在, 这样的元素是不会出现在elementSet()
和entrySet()
的返回结果中的. 而Map<E, Integer>
肯定不会有这样的限制.multiset.size()
返回所有元素出现次数之和, 如果想要知道有多少个不重复的元素, 可以使用elementSet().size()
, 例如{a,a,b}
,elementSet.size()
返回结果是 2,multiset.size()
返回结果是 3.multiset.iterator()
用于迭代每个出现的元素, 所以迭代次数和multiset.size()
的值一样的.Multiset<E>
支持增加元素, 删减元素, 或者通过setCount
函数直接设置元素的出现次数,setCount(a, 0)
的意思等于将删除所有的a
元素.multiset.count(elem)
: 如果元素elem
不存在, 那么返回值总是 0. 而Map
对于不存在的元素, 返回的是null
.
2.3 Multiset 实现
鉴于 Multiset
只是个接口, Guava 提供许多的接口实现, 大致可以与 Java
中的容器对应上:
Map | MultiSet | 支持 null 元素 |
---|---|---|
HashMap | HashMultiset | Yes |
TreeMap | TreeMultiset | Yes |
LinkedHashMap | LinkedHashMultiset | Yes |
ConcurrentHashMap | ConcurrentHashMultiset | No |
ImmutableMap | ImmutableMultiset | No |
3 Multimap
又来假设, 你是个班主任, 刚刚考完试, 你想记录下班里所有同学的成绩, 你可能写下这样的实现:
|
|
一个学生考试要考多个科目, 自然就会有多个学科成绩, 也就出现了一个 key
需要对应多个 value 的情况.
使用Map<K, List<V>>
或者Map<K, Set<V>>
这样的方式构建 key-values
自然可以, 只不过显得不甚优雅.
为此, Guava 提供了新的容器类型来应对一个
key 对应多个 values 的场景: Multimap
. 同样的,
我们也可以从两个角度来理解 Multimap
:
- 一个
key
对应一个value
, 同样的key
可以存在多个:
|
|
- 或者一个
key
对应一个列表的value
:
|
|
通常来说, 最好以第一种方式来理解 Multimap
接口,
不过你也可以以第二种方式来获取数据: asMap()
函数, 返回一个
Map<K, Collection<V>>
对象.
需要注意的是, 不存在 1 个 key
对应 0 个 value
的情况, 不会有空的值列表这样的说法, 要不一个 key
对应至少一个
value
, 要不就是这个 key
不存在于这个 Multimap
.
一般来说, 我们不会直接使用 Multimap
接口, 使用的是它的子接口; Multimap
接口提供了两个子接口: ListMultimap
和 SetMultimap
, 大致类似于
Map<K, List<V>>
和 Map<K, Set<V>>
.
3.1 Multimap 的例子
现在让我们用 Multimap
重新实现一次学生不同科目的成绩单:
|
|
3.1.1 构造
细心的同学可能会发现, 上面创建 ListMultimap
的方式不是直接调用实现类的.create()
函数, 而是使用 MultimapBuilder
.
并不是 Multimap
的实现没有提供.create()
方法, 是通过
MultimapBuilder
创建 Multimap
实现会更加便利一点,
使用hashKeys()
函数创建的就是一个 HashMap
,
使用treeKeys()
函数创建的就是一个 TreeMap
.
3.1.2 修改
Multimap.get(key)
返回的就是特定 key
关联的集合, 对于一个
ListMultimap
, 返回的就是一个 List
; 对于一个 SetMultimap
,
返回的就是一个 Set
.
实际返回的是集合的引用, 所以对这个返回集合的操作,
将直接反馈在 Multimap
实例上. 如上面的例子所示, 把学生 alan
返回列表的数据清空, 在 ListMultimap
的数据也相应地被清空了.
3.2 视图
所谓的视图(Views), 我理解就是看待事物的方式和角度, 称为视图(或者视角’perspective’).
Multimap
提供了若干个有用的视图:
asMap
把Multimap<K,V>
看作一个Map<K, Collection<V>>
, 返回一个的 map 对象支持remove
操作, 但不支持put
和putAll
操作.值得关注的是: 当对应的 key 不存在的时候, multiMap 返回的是一个新构造的, 为空的集合, 如果你想在对应的 key 不存在的时候返回空指针(就好像 HashMap 那样), 你可以通过
asMap().get(key)
实现这样的效果
|
|
entries
把Multimap
内所有的记录(entry)看作Collection<Map.Entry<K,V>>
, 如前文的studentScoresMap.entries()
返回的就是:[{"Alan": 95}, {"Alan": 88}, {"Turing": 100}]
.keySets
把Multimap
内所有的不重复的key
看作一个Set
. 如前文的studentScoresMap.keySets()
返回的就是:Set(["Alan","Turing"])
.keys
把Multimap
内所有的 key 看作一个前文提到的Multiset
, 可以从这个Multiset
删除元素, 但不能新增元素, 如前文的studentScoresMap.keys()
返回的就是:Multiset(["Alan","Alan", "Turing"])
.values()
把Multimap
内所有的 value 看作一个集合, 相当于把所有 key 对应的 value 集合串联起来, 如前文的studentScoresMap.values()
返回的就是:[95, 88, 100]
3.3 Multimap 也不是一个 Map
严格来说, 即使 Multimap
名字中带有 map, 甚至 map
可能用来实现
Multimap
, 但一个 Multimap<K,V>
终究不是一个
Map<K, Collection<V>>
. 它们之间的差异包括:
Multimap.get(key)
返回的对象总是不为空指针的, 即使查询的 key 不存在, 返回的是个空的集合. 而Map.get(key)
查询的 key 不存在, 返回的就是空指针. 前文提到过, 如果想要让Multimap
在查询 key 不存在的时候返回空指针, 可以使用Multimap.asMap().get(key)
.Multimap.containsKey(key)
在 values 集合为空的时候就会返回 false, 例如studentScoresMap.putAll("elon", Lists.newArrayList()); Assert.assertFalse(studentScoresMap.containsKey("elon"))
, 但对于Map<K, Collection<V>>
而言, 返回的就会是 true, 因为 value 不为 null.Multimap.size()
返回的是所有记录的总数的, 即把所有的 value 的数量累加起来, 而Map<K, Colleciton<V>>
返回的就是 key 对应的数量.
3.4 实现
Multimap
提供了若干个不同类型的实现, 你可以使用对应的实现来取代原来
Map<K, Collection<V>>
的地方:
实现 | key 表现得类似… | value 表现得类似… |
---|---|---|
ArrayListMultimap | HashMap | ArrayList |
HashMultimap | HashMap | HashSet |
LinkedListMultimap | LinkedHashMap | LinkedList |
LinkedHashMultimap | LinkedHashMap | LinkedHashSet |
TreeMultimap | TreeMap | TreeSet |
ImmutableListMultimap | ImmutableMap | ImmutableList |
ImmutableSetMultimap | ImmutableMap | ImmutableSet |
上述的实现, 除了不可变的实现之外, 其他都支持 null key
与 null value
.
并非所有的实现底层用的都是 Map<K, Collection<V>>
,
有好几个实现出于性能的考虑, 实现了自定义的 hash 表.
Multimap
还支持自定义 value 的集合形式, 如 List 形式或者 Set 形式, 详情可见
Multimaps.newMultimap(Map, Supplier<Collection>)
4 BiMap
继续假设, 你是个班主任, 你有个学生名字与学号的名单, 你有时会通过名字查询对应学号, 有时又会根据学号反查询学生名字, 通常来说, 你会这么实现这个名单:
|
|
不得不说, 通过两个 Map 和实现 value
反查 key
的传统做法并不优雅,
即增加了心理负担, 又容易出 bug.
幸运的是, Guava 有一个名为 BiMap
类库, 提供了通过 value
也反查 key
的特性.
一个BiMap<K,V>
是一个Map<K,V>
, 提供了如下功能:
- 允许通过
inverse()
函数调转 key-value, 从Map<K,V>
变成Map<V,K>
- 保证所有的
value
都是唯一的,values()
函数返回一个包含所有value
的 Set - 如果
value
已经存在, 那么BiMap.put(key,value)
会抛出一个IllegalArgumentException
异常, 如果想强制删除掉原来的value
, 并插入一对新的 key-value, 可以使用Bimap.forcePut(key,value)
4.1 BiMap 例子
让我们用 BiMap 来重新实现学生名字和学号的名单:
|
|
4.2 BiMap 实现
key-value map 实现 | value-key map 实现 | 对应的 BiMap |
---|---|---|
HashMap | HashMap | HashBiMap |
ImmutableMap | ImmutableMap | ImmutableBiMap |
EnumMap | EnumMap | EnumBiMap |
EnumMap | HashMap | EnumHashBiMap |
5 Table
假设还是个班主任, 现在你需要制作一个包含学号, 姓名与成绩的名单, 然后可以通过姓名或者学号进搜索, 你会怎么实现呢? 什么? 用 excel? 你好幽默啊!
|
|
不得不说, 用 Map<R, Map<C, V>>
的形式来实现多 key 搜索非常难受,
算法效率变为 O(n), 线性时间复杂度, 不但不优雅, 还容易出错,
如果我是班主任, 我就辞职了, 给我个 excel 不行么?
excel 是没有的了, 但是 Guava 提供了一个类 excel 的多 key 存储/搜索的容器: Table
,
它支持以行和列维度搜索.
5.1 Table 例子
让我们用 Table
重新实现一次可根据姓名与学号进行搜索的成绩单:
|
|
正常来说, 不会有两个学号一样的学生, 只是为了展示 Table
用法而这样造数据. row
, column
函数可能让人比较迷惑,
这两个函数是怎么搜索的?
其实很简单, row
是以第一个 key 来搜索, 而 column
以第二个 key 来搜索, 如图:
5.2 Table 视图
一往常, Table
也提供了若干个视图:
rowMap()
, 把Table<R, C, V>
看作一个Map<R, Map<C, V>>
, 同样的,rowKeySet()
返回一个Set<R>
.row(r)
返回一个非空的Map<C, V>
的引用, 对返回的 Map 的修改也会反馈给持有该引用Table
.- 类似地,
column(c)
返回一个非空的Map<R, V>
的引用, 对返回的 Map 的修改也会反馈给持有该引用Table
. cellSet()
把Table<R, C, V>
看作一个Table.Cell<R, C, V>
,Cell
与Map.Entry
十分类似, 只不过它有两个 key, 形式是(r,c)=v
, 而Map.Entry
是key = value
.
5.3 Table 实现
Table 依旧提供了若干个实现, 列表如下:
Table<R, C, V> | 类似的 Map<R, Map<C, V>> |
---|---|
HashBasedTable | HashMap<R, HashMap<C, V>> |
TreeBasedTable | TreeMap<R, TreeMap<C, V>> |
ImmutableTable | ImmutableMap<R, ImmutableMap<C, V>> |
ArrayTable | ImmutableMap<R, ImmutableMap<C, V>>, 特别的一个 |
6 ClassToInstanceMap
目前, 我们介绍过的 Map, 无论是原生 Jdk 的 Map, 抑或是 Guava 的 Map,
key
都是同一个类型的.
这是因为 Map 的签名是 Map<K,V>
, 实例的时候, 只能实例成某具体一个类型的参数. 所谓凡事都有例外, 有没有支持 key
是不同类型的 map 呢? 自然是有的, Guava 的
ClassToInstanceMap
就可以支持多个类型的 key.
为什么它可以实现多个类型的 key
呢? 因为 ClassToInstanceMap
的签名声明为 Map<Class<? extends B>, B>
,
通过传入不同类型的 Class
对象, 实现类型不同的 =key=(如果你要说,
即使传入不同类型的 Class 对象, 它只有一个 Class, 没有实现多个不同类型的
key 阿! 你也可以这样理解, well, 咬文嚼字就没有什么意义了)
6.1 ClassToInstanceMap 例子
|
|
如果查看源码, 可以发现, ClassToInstanceMap<B>
只有一个类型参数 B
:
|
|
很明显的, 类型 B
限制了 key
与 value
的类型。
对于 value
的限制,
就和常规的 map
一样; 而对于 key
而言, 泛型实例化时的参数类型只能是
B
, 或者是 B
的子类, 例如: ClassToInstanceMap<Number>
, 那么这个 map
的 key
类型必须是 Number
或 Number
的子类, 而传入的 Integer
和
Long
都是 Number
子类, 因此能编译通过。
如果传入的是 String,
不符合声明, 编译就报错了.
需要注意的是, 和 Map<Class, Object>
一样, 一个 ClassToInstanceMap
可以包含着是原始类型的 value
,
而原始类型与它对应的包装类型并不是同一种类型, 不要混淆了哦
6.2 ClassToInstanceMap 实现
ClassToInstanceMap
提供了两个实现:
ClassToInstanceMap | 类似的 Map |
---|---|
MutableClassToInstanceMap | Map<Class, Object> |
ImmutableClassToInstanceMap | ImmutableMap<Class,Object> |
7 RangeSet
目前为止, 我们介绍过的新类型容器都是常见的 Map/Set/Table,
现在我们就来介绍一个表示区间的容器: RangeSet
. 一个 RangeSet
,
表示一个包含无连接的, 不为空的区间的集合, 例如包含一个整数区间的
RangeSet
: {[1,5], [7,9)}
.
在 RangeSet
中, 区间是由类 Range
来表示的, 当把一个区间加入到一个可变的 RangeSet
时,
任何有交集的区间都会被合并, 为空的区间就会被忽略, 例如将区间 [3,5]
加入到已有的 RangeSet
{[2,4]}
, 就会被合并成 {[2,5]}
,
这个也符合我们日常的生活经验.
7.1 RangeSet 例子
让我们现在来看一下 RangeSet
的两个例子, 一个是整数的 RangeSet
|
|
在上面的例子中, [2,4]
和 [3,5]
这两个区间有交集,
所以它们被自动合并到一起了, 而对于区间 [1,10]
和 [11,15)
, 10
相邻的整数就是 11, 但两个区间也没有合并起来, 因为它们没有相交,
如果想要他们合并起来, 可以手动调用 Range.canonical(DiscreteDomain)
,
即:
|
|
另外一个例子是日期的 RangeSet
:
|
|
RangeSet
提供了若干个查询函数, 用法在上面的代码已经展示了,
查询函数列表:
contains(C)
:RangeSet
最基础的查询操作, 判断任意的元素是否在RangeSet
内.rangeContaining(C)
: 与contains(C)
类似, 判断任意的元素是否在RangeSet
内, 如果在的话返回一个对应的区间, 否则返回空指针. 如上代码, 有RangeSet
:{[2019-10-10,2019-10-20), (2019-10-30, 2019-12-30)}
, 而元素2019-10-19
在区间[2019-10-10, 2019-10-20)
内, 因此rangeContaining(C)
函数返回的就是[2019-10-10, 2019-10-20)
.encloses(Range<C>)
: 判断任意的区间是否在RangeSet
的包围中.span
: 返回一个最小区间, 包含RangeSet
中的所有区间, 如有:RangeSet
:{[2019-10-10,2019-10-20), (2019-10-30, 2019-12-30)}
,span
函数返回的区间就是{[2019-10-10, 2019-12-30)}
.
7.2 RangeSet 视图
依照惯例, RangeSet
也提供了若干个视图:
complement()
: 返回某个RangeSet
的补集, 返回结果也是个RangeSet
, 如有RangeSet
:{[2019-10-10,2019-10-20), (2019-10-30, 2019-12-30)}
, 那它的补集就是:RangeSet
:{(-∞,2019-10-10), [2019-10-20,2019-10-30], [2019-12-30,+∞)}
, 分别是三个区间: 负无穷到2019-10-10
,2019-10-20
到2019-10-30
, 以及2019-12-30
到正无穷.subRangeSet(Range<C>)
: 返回某个RangeSet
相交的子区间, 如有RangeSet
:{[2019-10-10,2019-10-20), (2019-10-30, 2019-12-30)}
, 取子区间[2019-11-10,2019-11-20]
, 那么返回结果就是{[2019-11-10, 2019-11-20]}
; 如果取子区间[2019-10-15, 2019-11-20]
, 那么返回结果就是{[2019-10-10, 2019-10-20), (2019-10-30, 2019-11-20]}
asRanges()
: 把RangeSet
当作一个Set<Range<C>>
, 如有RangeSet
:{[2019-10-10,2019-10-20), (2019-10-30, 2019-12-30)}
, 返回结果就是:Set({[2019-10-10,2019-10-20), (2019-10-30, 2019-12-30)})
7.3 RangeSet 实现
RangeSet
提供了两个实现:
RangeSet | 类似的 Set<Range> |
---|---|
TreeRangeSet | TreeSet<Range> |
ImmutableRangeSet | ImmutableSet<Range> |
8 RangeMap
既然能以区间集作为容器, 那么能否把区间当作 Map 的 key
呢?
答案是当然可以, Guava 就提供了一个这样的容器: RangeMap
.
需要注意的是, 不像 RangeSet
那样, 相邻或者相交的区间不能连接起来的,
即使毗邻的区间映射的是同一个 value
.
9 RangeMap 例子
|
|
RangeMap
提供的查询函数不多, 满打满算也只有 get(K)
和 span
这两个函数.
9.1 RangeMap 视图
RangeMap
提供的视图也不多, 只有两个:
asMapOfRanges()
, 把RangeMap
看作一个Map<Range<K>, V>
, 可以用来遍历RangeMap
subRangeMap(Range<K>)
, 返回某个RangeMap
相关区间的子区间以及对应的value
, 如有RangeMap
:{[1, 3] => "foo", (3, 5) => "bar", (11, 20) => "foo"
, 取子区间[12,15]
, 返回结果就是{[12, 15]} => "foo"
; 如果取子区间[4,12]
, 返回结果就是:{[4,5) => bar, (11, 12] => foo}
9.2 RangeMap 实现
RangeMap 提供了两个实现:
RangeMap | 类似的 Map<Range, V> |
---|---|
TreeRangeMap | TreeMap<Range, V> |
ImmutableRangeMap | ImmutableMap<Range<K, V>> |
10 总结
介绍完新类型的容器之后, 希望大家对这些新类型容器熟悉起来, 应对需求来也能得心应手 :)