如何在Java中的Map中存储重复的键?

评论 0 浏览 0 2018-04-30

1.概述

在本教程中,我们将探讨处理具有重复键的Map的可用选项,或者换句话说,一个允许为一个键存储多个值的Map

2.标准Map

Java有几种接口Map的实现,每一种都有自己的特点。

但是,现有的 Java 核心 Map 实现均不允许 Map 处理单个键的多个值。

正如我们所看到的,如果我们试图为同一个键插入两个值,第二个值将被存储,而第一个值将被丢弃。

它也将被返回(通过每一个适当的put(K key, V value)方法的实现)。

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作为值

显然,为我们的Map的每一个值使用一个Collection就可以完成工作。

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");

然而,这种冗长的解决方案有多个缺点,而且容易出错。它意味着我们需要为每个值实例化一个Collection,在添加或删除一个值之前检查它是否存在,在没有值的时候手动删除它,等等。

从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");

然而,我们应该避免这样做,除非有非常充分的理由不这样做,例如公司的限制性政策阻止我们使用第三方的库。

否则,在编写我们自己的自定义Map实现之前,我们应该在开箱即用的几个选项中进行选择。

4. Apache Commons Collections

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

让我们从导入最新版本的Common Collections(从现在开始是CC)开始。

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

4.1. MultiMap

org.apache.commons.collections4。MultiMap接口定义了一个Map,它持有针对每个键的值集合。

它是由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.collection4.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装饰器来使它们成为不可变的。

@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是谷歌核心库的Java API。

让我们先在我们的项目中导入Guava。

<dependency>
  <groupId>com.google.guava</groupId>
  <artifactId>guava</artifactId>
  <version>31.0.1-jre</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接口的不可变的实现。com.google.common.collect.ImmutableListMultimapcom.google.com.collect.ImmutableSetMultimap

5.1.常见的Map实现

当我们需要一个特定的Map实现时,首先要做的是检查它是否存在,因为Guava很可能已经实现了它。

例如,我们可以使用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

还有许多其他的实现方式可供选择。

然而,我们可能想装饰一个Map和/或一个List,这还没有实现。

幸运的是,Guava有一个工厂方法允许我们这样做 — Multimap.newMultimap()

6.结语

我们已经看到了如何在Map中以现有的所有主要方式为一个键存储多个值。

我们已经探讨了Apache Commons Collections和Guava的最流行的实现,在可能的情况下,应该优先选择这些实现而不是定制的解决方案。

一如既往,完整的源代码可在GitHub上获得。

最后更新2023-01-17
0 个评论
标签