引言
在 Java 开发中,JDK 提供的集合框架(如 List、Set、Map)已经能够满足大部分日常需求。然而,当面对更复杂的数据关系时,开发者往往需要手动组合这些基础集合(例如 Map<K, List<V>>),这不仅代码冗长,还容易引发空指针异常或逻辑错误。
Google Guava 库引入了一系列强大的新集合类型,旨在填补 JDK 的空白。这些集合类型设计精良,与 JDK 集合框架完美共存,能够以更简洁、更安全的方式处理多维数据映射、计数、双向查找及范围查询等场景。本文将详细介绍 Guava 中的核心新集合类型:Multiset、Multimap、BiMap、Table、ClassToInstanceMap、RangeSet 和 RangeMap。
1. Multiset:高效的元素计数器
痛点
在传统 Java 中,统计元素出现次数通常需要使用 Map<E, Integer>,代码繁琐且易错:
java
Map<String, Integer> counts = new HashMap<>();
for (String word : words) {
Integer count = counts.get(word);
if (count == null) {
counts.put(word, 1);
} else {
counts.put(word, count + 1);
}
}
解决方案
Guava 的 Multiset 允许元素重复出现,它结合了 List(无序集合)和 Map(元素到计数的映射)的特性。
核心特性:
- 作为 Collection :
add(E)增加一个实例,size()返回所有实例的总数,迭代器遍历每个实例。 - 作为 Map 视图 :
count(E)返回元素出现次数,elementSet()返回去重后的元素集合。 - 内存优化:内存消耗仅与不同元素的数量成线性关系,而非总实例数。
常用方法:
| 方法 | 描述 |
|---|---|
count(E) |
返回元素出现的次数 |
add(E, int) |
增加指定数量的该元素实例 |
setCount(E, int) |
直接设置元素的计数值 |
elementSet() |
获取所有不同元素的集合视图 |
entrySet() |
获取 {元素, 计数} 的条目集合视图 |
实现类推荐:
HashMultiset:对应HashMap,支持 null,查询 O(1)。TreeMultiset:对应TreeMap,支持排序。ConcurrentHashMultiset:线程安全版本。
注意 :
Multiset不是Map。它不包含计数为 0 的元素,且size()返回的是总实例数而非不同元素个数。
2. Multimap:一对多映射的优雅解法
痛点
处理"一个键对应多个值"的场景(如图论中的邻接表)时,开发者常使用 Map<K, List<V>>。这导致每次 put/get 都需要判断列表是否存在,代码臃肿。
解决方案
Multimap 将键映射到值的集合,简化了操作逻辑。它可以被视为一组键值对映射,也可以视为键到集合的映射。
核心特性:
- 自动初始化 :
get(key)永远返回一个非 null 的集合视图(即使该键尚不存在)。对该集合的修改会直接写回 Multimap。 - 灵活视图 :
asMap():将其视为Map<K, Collection<V>>。keys():将键视为Multiset,反映每个键关联的值数量。values():将所有值扁平化为一个大的Collection<V>。
构建方式:
推荐使用 MultimapBuilder 进行类型安全的构建:
java
// 创建一个 Key 为树结构,Value 为 ArrayList 的 Multimap
ListMultimap<String, Integer> treeListMultimap =
MultimapBuilder.treeKeys().arrayListValues().build();
常用操作:
put(K, V):添加单个映射。get(K):获取值集合视图。removeAll(K):移除某键关联的所有值。replaceValues(K, Iterable<V>):替换某键关联的所有值。
实现类推荐:
ArrayListMultimap/HashMultimap:最常用,分别对应 List 和 Set 语义。LinkedHashMultimap:保持插入顺序。ImmutableListMultimap:不可变版本,线程安全。
3. BiMap:双向唯一映射
痛点
需要反向查找(通过 Value 找 Key)时,通常维护两个 Map 并手动同步,极易出错。
解决方案
BiMap(双向 Map)强制 Key 和 Value 都是唯一的,并提供 inverse() 方法获取反向视图。
核心特性:
- 值唯一性 :
put(key, value)时,如果 value 已存在,会抛出IllegalArgumentException。 - 强制覆盖 :若需覆盖已存在的 value 映射,使用
forcePut(key, value)。 - 反向视图 :
biMap.inverse().get(value)即可获取对应的 key,无需额外维护反向 Map。
实现类:
HashBiMap:基于哈希表。EnumBiMap/EnumHashBiMap:针对枚举类型优化。ImmutableBiMap:不可变版本。
4. Table:二维映射结构
痛点
处理类似矩阵或表格的数据(如 Map<Row, Map<Column, Value>>)时,嵌套 Map 的访问和遍历非常不便。
解决方案
Table<R, C, V> 提供了原生的二维映射支持,通过行(Row)和列(Column)两个键来定位值。
核心特性:
- 多维度视图 :
row(R):返回该行所有列数据的 Map 视图。column(C):返回该列所有行数据的 Map 视图。cellSet():返回所有单元格{Row, Column, Value}的集合。
- 便捷访问 :直接通过
table.get(rowKey, columnKey)获取值。
实现类:
HashBasedTable:底层由HashMap<R, HashMap<C, V>>支持,最通用。TreeBasedTable:支持行列排序。ArrayTable:当行列空间固定且密集时,性能极高(基于二维数组)。
5. ClassToInstanceMap:类型安全的类到实例映射
痛点
当 Map 的 Key 是 Class 对象,Value 是该 Class 的实例时,传统 Map<Class<T>, T> 在获取时需要强制类型转换,既不安全也不优雅。
解决方案
ClassToInstanceMap<B> 扩展了 Map 接口,提供了泛型安全的方法:
T getInstance(Class<T>):直接返回正确类型的实例,无需强转。T putInstance(Class<T>, T):存入实例。
示例:
java
ClassToInstanceMap<Number> numberDefaults = MutableClassToInstanceMap.create();
numberDefaults.putInstance(Integer.class, Integer.valueOf(0));
// 获取时自动推断为 Integer,无需 (Integer) 强转
Integer i = numberDefaults.getInstance(Integer.class);
6. RangeSet 与 RangeMap:范围数据处理
RangeSet:不连续范围的集合
用于管理一组不相交的区间。添加区间时,相邻或重叠的区间会自动合并。
特性:
- 自动合并 :添加
[1, 10]和[11, 20](若离散域连续)可能合并为大区间。 - 丰富查询 :
contains(value):判断值是否在任意区间内。rangeContaining(value):返回包含该值的具体区间。complement():获取补集。subRangeSet(range):获取交集视图。
RangeMap:范围到值的映射
将不相交的区间映射到具体的值。与 RangeSet 不同,RangeMap 不会自动合并相邻且值相同的区间(除非显式操作),保持映射的精确性。
特性:
- 区间切割:放入新区间时,若与现有区间重叠,会自动切割现有区间以保证不相交。
- 视图 :
asMapOfRanges()可将其视为Map<Range<K>, V>进行遍历。
注意 :
RangeSet和RangeMap依赖 JDK 1.6+ 的NavigableMap特性。
总结
Guava 的新集合类型不仅仅是 API 的扩充,更是编程思维的升级。它们将常见的复杂数据结构模式(计数、一对多、双向映射、二维表、范围查询)封装为独立、高效且类型安全的抽象。
- 需要计数 ?用
Multiset代替Map<E, Integer>。 - 需要一对多 ?用
Multimap告别嵌套 Map 的判断逻辑。 - 需要反向查找 ?用
BiMap确保数据一致性。 - 需要二维索引 ?用
Table简化矩阵操作。 - 需要范围管理 ?用
RangeSet/RangeMap处理区间逻辑。