1. 概述

本文将深入探讨 Google Guava 库中的核心数据结构——Multimap。它是一种特殊的键值对集合,与 java.util.Map 类似,但允许一个键关联多个值。这种设计在处理一对多关系时特别实用。

2. 依赖配置

首先在 pom.xml 中添加 Guava 依赖:

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>33.3.0-jre</version>
</dependency>

最新版本可在此处获取:Maven Central

3. Multimap 实现原理

Guava Multimap 的核心特性:当向同一个键添加多个值时,新值不会覆盖旧值,而是追加到集合中。看个测试用例:

String key = "a-key";
Multimap<String, String> map = ArrayListMultimap.create();

map.put(key, "firstValue");
map.put(key, "secondValue");

assertEquals(2, map.size());

打印 map 内容会输出:

{a-key=[firstValue, secondValue]}

通过键获取值时,会得到包含所有值的 Collection<String>

Collection<String> values = map.get(key);

输出结果:

[firstValue, secondValue]

4. 遍历 Multimap 的方式

Multimap 提供了多种内置遍历方法,我们逐一解析。先准备测试数据:

Multimap<String, String> multiMap = ArrayListMultimap.create();
multiMap.putAll("key1", List.of("value1", "value11", "value111"));
multiMap.putAll("key2", List.of("value2", "value22", "value222"));
multiMap.putAll("key3", List.of("value3", "value33", "value333"));

4.1. 使用 entries() 方法

最直接的方式:entries() 返回所有键值对的集合视图。实现代码:

static void iterateUsingEntries(Multimap<String, String> multiMap) {
    multiMap.entries()
      .forEach(entry -> LOGGER.info("{} => {}", entry.getKey(), entry.getValue()));
}

执行输出:

key1 => value1
key1 => value11
key1 => value111
key2 => value2
key2 => value22
key2 => value222
key3 => value3
key3 => value33
key3 => value333

每个 entry 都是 Map.Entry 实例,包含完整的键值对信息。

4.2. 使用 asMap() 方法

asMap() 将 Multimap 转换为标准 Map 视图,其中值是原始集合:

static void iterateUsingAsMap(Multimap<String, String> multiMap) {
    multiMap.asMap()
      .entrySet()
      .forEach(entry -> LOGGER.info("{} => {}", entry.getKey(), entry.getValue()));
}

输出结果:

key1 => [value1, value11, value111]
key2 => [value2, value22, value222]
key3 => [value3, value33, value333]

⚠️ 注意:这里每个键对应的值是整个集合,而非单个值。

4.3. 使用 keySet() 方法

仅遍历所有唯一键,返回 Set<String>

static void iterateUsingKeySet(Multimap<String, String> multiMap) {
    multiMap.keySet()
      .forEach(LOGGER::info);
}

输出:

key1
key2
key3

使用方法引用简化代码,适合只需处理键的场景。

4.4. 使用 keys() 方法

keys() 返回包含所有键的集合(含重复),本质是 Multiset

static void iterateUsingKeys(Multimap<String, String> multiMap) {
    multiMap.keys()
      .forEach(LOGGER::info);
}

输出:

key1
key1
key1
key2
key2
key2
key3
key3
key3

✅ 与 keySet() 的区别:每个值对应的键都会出现一次,适合需要统计键频次的场景。

4.5. 使用 values() 方法

直接获取所有值的集合,忽略键信息:

static void iterateUsingValues(Multimap<String, String> multiMap) {
    multiMap.values()
      .forEach(LOGGER::info);
}

输出:

value1
value11
value111
value2
value22
value222
value3
value33
value333

⚠️ 重要:返回的集合与底层 Multimap 双向绑定,修改会相互影响,但无法直接添加新元素。

5. 与标准 Map 的对比

标准 java.util.Map 不支持一键多值。看个踩坑案例:

String key = "a-key";
Map<String, String> map = new LinkedHashMap<>();

map.put(key, "firstValue");
map.put(key, "secondValue");

assertEquals(1, map.size()); // 实际只有 secondValue

要实现类似 Multimap 的效果,需要手动处理:

String key = "a-key";
Map<String, List<String>> map = new LinkedHashMap<>();

List<String> values = map.get(key);
if(values == null) {
    values = new LinkedList<>();
    values.add("firstValue");
    values.add("secondValue");
 }
map.put(key, values);

assertEquals(1, map.size()); // 虽然列表有2个元素,但map.size()=1

这种写法既啰嗦又容易出错,此时 Multimap 就是更优雅的选择。

6. Multimap 的核心优势

Multimap 主要用于替代 Map<K, Collection<V>> 的场景,优势包括:

  • 简化初始化:添加值前无需手动创建空集合
  • 安全获取get() 永远返回空集合而非 null,避免 NPE
  • 自动清理:当键关联的所有值被移除时,键会自动删除(标准 Map 会保留空集合造成内存浪费)
  • 真实计数size() 返回实际存储的值总数,keySet().size() 返回唯一键数
  • 丰富 API:提供多种内置遍历方法,简化开发

7. 总结

本文深入探讨了 Guava Multimap 的使用场景和实现原理,通过对比标准 Map 突出了其在一对多关系处理中的优势。重点解析了五种遍历方式:

  1. entries():遍历所有键值对
  2. asMap():转换为标准 Map 视图
  3. keySet():遍历唯一键
  4. keys():遍历所有键(含重复)
  5. values():遍历所有值

当你的代码中出现 Map<K, List<V>>Map<K, Set<V>> 时,强烈建议考虑使用 Multimap 重构,代码将更简洁高效。


原始标题:Iterate over a Guava Multimap | Baeldung