1. 概述

本教程中,我们将探索如何处理存在key重复的Map,换而言之,如何在Map中允许单个key保存多个值。

2. 标准 Map 实现类

Java 中有几个Map接口的实现类。

但是,现有的Map实现类中都不允许处理单个key对应多个值的情况

如果我们尝试向同一个key中插入两个值,则只会保留第二个值,第一值会被覆盖掉。

同时put()方法会返回旧的值。

Map<String, String> map = new HashMap<>();
assertThat(map.put("key1", "value1")).isEqualTo(null);
assertThat(map.put("key1", "value2")).isEqualTo("value1");
assertThat(map.get("key1")).isEqualTo("value2");

那么如何才能实现我们想要的结果呢?

3. 方法一,借助Collection

显然我们可以借助Collection来记录key重复的值:

    Map<String, List<String>> map = new HashMap<>();
    List<String> list = new ArrayList<>();
    map.put("key1", list);
    map.get("key1").add("value1");
    map.get("key1").add("value2");
     
    assertThat(map.get("key1").get(0)).isEqualTo("value1");
    assertThat(map.get("key1").get(1)).isEqualTo("value2");

但是,这种冗长的解决方案缺点很多,并且容易出错。这要求我们需要为每个key都实例化一个集合,在添加或删除一个值之前检查集合是否存在,当没有值时手动删除它,等等。

Java 8 中我们可以利用compute()方法对其稍稍改进:

    Map<String, List<String>> map = new HashMap<>();
    map.computeIfAbsent("key1", k -> new ArrayList<>()).add("value1");
    map.computeIfAbsent("key1", k -> new ArrayList<>()).add("value2");
    
    assertThat(map.get("key1").get(0)).isEqualTo("value1");
    assertThat(map.get("key1").get(1)).isEqualTo("value2");

这种方法知道即可,一般不推荐使用,更好的办法是利用第三方库,而不是自己造轮子。

4. 利用第三方库 - Apache Commons Collections

像往常一样,Apache为我们的问题提供了解决方案。

让我们先通过Maven导入最新版本依赖(当前):

<dependency>
  <groupId>org.apache.commons</groupId>
  <artifactId>commons-collections4</artifactId>
  <version>4.1</version>
</dependency>

4.1. MultiMap

org.apache.commons.collections4.MultiMap 接口定义一个Map,该Map保存每个key对应一组值的集合。

它的实现类是org.apache.commons.collections4.map.MultiValueMap

MultiMap<String, String> map = new MultiValueMap<>();
map.put("key1", "value1");
map.put("key1", "value2");
assertThat((Collection<String>) map.get("key1"))
  .contains("value1", "value2");

虽然此类从CC 3.2版本开始可用,但它不是线程安全的,并且在CC 4.1中已弃用。 因此仅当我们无法升级到较新版本时,才应使用它。

4.2. MultiValuedMap

MultiMap的继承者是org.apache.commons.collections4.MultiValuedMap接口。它有多个实现类。

下面我们看下如何把多个值保存到ArrayList中,并保留重复的值:

MultiValuedMap<String, String> map = new ArrayListValuedHashMap<>();
map.put("key1", "value1");
map.put("key1", "value2");
map.put("key1", "value2");
assertThat((Collection<String>) map.get("key1"))
  .containsExactly("value1", "value2", "value2");

另外,我们可以使用HashSet,它会去掉重复的值:

MultiValuedMap<String, String> map = new HashSetValuedHashMap<>();
map.put("key1", "value1");
map.put("key1", "value1");
assertThat((Collection<String>) map.get("key1"))
  .containsExactly("value1");

上面两个实现都不是线程安全地。

让我们看看如何使用UnmodifiableMultiValuedMap装饰器使它们不可变(immutable):

@Test(expected = UnsupportedOperationException.class)
public void givenUnmodifiableMultiValuedMap_whenInserting_thenThrowingException() {
    MultiValuedMap<String, String> map = new ArrayListValuedHashMap<>();
    map.put("key1", "value1");
    map.put("key1", "value2");
    MultiValuedMap<String, String> immutableMap =
      MultiMapUtils.unmodifiableMultiValuedMap(map);
    immutableMap.put("key1", "value3");
}

5. 使用 Guava Multimap

Guava 是 Google 开源的Java核心工具库。

com.google.common.collect.Multimap 接口自版本2开始就存在。在撰写本文时,Guava最新版本是25,版本号23之后,Guava被分为jre和android(25.0-jre和25.0-android),我们的示例仍将使用版本号23。

导入Guava依赖:

<dependency>
  <groupId>com.google.guava</groupId>
  <artifactId>guava</artifactId>
  <version>23.0</version>
</dependency>

Guava 从一开始就遵循了多种实现方式。

最常用的是com.google.common.collect.ArrayListMultimap,它使用一个HashMap背后使用ArrayList存储每个值:

Multimap<String, String> map = ArrayListMultimap.create();
map.put("key1", "value2");
map.put("key1", "value1");
assertThat((Collection<String>) map.get("key1"))
  .containsExactly("value2", "value1");

与往常一样,我们应该首选Multimap接口的Immutable实现: com.google.common.collect.ImmutableListMultimapcom.google.common.collect.ImmutableSetMultimap

5.1. 常用实现类

com.google.common.collect.LinkedHashMultimap ,保留键值插入顺序:

Multimap<String, String> map = LinkedHashMultimap.create();
map.put("key1", "value3");
map.put("key1", "value1");
map.put("key1", "value2");
assertThat((Collection<String>) map.get("key1"))
  .containsExactly("value3", "value1", "value2");

com.google.common.collect.TreeMultimap ,按自然顺序排序:

Multimap<String, String> map = TreeMultimap.create();
map.put("key1", "value3");
map.put("key1", "value1");
map.put("key1", "value2");
assertThat((Collection<String>) map.get("key1"))
  .containsExactly("value1", "value2", "value3");

5.2. 自定义MultiMap

Guava 还提供了其他许多实现。

但可能我们需要的并未实现,幸运的是 Guava 提供了一个工厂方法允许我们自定义实现:Multimap.newMultimap()

6. 总结

本文我们学习了使用几种不同的方式,包括利用Apache Commons Collections和Guava,来保存重复key的多个值。

示例完整代码可从 Github 上获取。