How to Store Duplicate Keys in a Map in Java? – 如何在Java中的地图中存储重复的键?

最后修改: 2018年 4月 30日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

In this tutorial, we’re going to explore the available options for handling a Map with duplicate keys or, in other words, a Map that allows storing multiple values for a single key.

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

2. Standard Maps

2.标准地图

Java has several implementations of the interface Map, each one with its own particularities.

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

However, none of the existing Java core Map implementations allow a Map to handle multiple values for a single key.

然而,现有的Java核心Map实现中,没有一个允许Map处理单个键的多个值。

As we can see, if we try to insert two values for the same key, the second value will be stored, while the first one will be dropped.

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

It will also be returned (by every proper implementation of the put(K key, V value) method):

它也将被返回(通过每个适当的实现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");

How can we achieve the desired behavior, then?

那么,我们如何才能实现理想的行为呢?

3. Collection as Value

3.作为价值的收藏

Obviously, using a Collection for every value of our Map would do the job:

很明显,为我们的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");

However, this verbose solution has multiple drawbacks and is prone to errors. It implies that we need to instantiate a Collection for every value, check for its presence before adding or removing a value, delete it manually when no values are left, etc.

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

From Java 8, we could exploit the compute() methods and improve it:

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

However, we should avoid it unless having a very good reason not to, such as restrictive company policies preventing us from using third-party libraries.

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

Otherwise, before writing our own custom Map implementation and reinventing the wheel, we should choose among the several options available out-of-the-box.

否则,在编写我们自己的自定义Map实现和重新发明轮子之前,我们应该在开箱即用的几个选项中选择。

4. Apache Commons Collections

4.Apache Commons集合

As usual, Apache has a solution for our problem.

像往常一样,Apache对我们的问题有一个解决方案。

Let’s start by importing the latest release of Common Collections (CC from now on):

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

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

4.1. MultiMap

4.1.MultiMap

The org.apache.commons.collections4.MultiMap interface defines a Map that holds a collection of values against each key.

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

It’s implemented by the org.apache.commons.collections4.map.MultiValueMap class, which automatically handles most of the boilerplate under the hood:

它是由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");

While this class is available since CC 3.2, it’s not thread-safe, and it’s been deprecated in CC 4.1. We should use it only when we can’t upgrade to the newer version.

虽然这个类从CC 3.2开始就有了,但它不是线程安全的,而且它在CC 4.1中已经被废弃了。我们应该只在无法升级到新版本的情况下使用它。

4.2. MultiValuedMap

4.2.MultiValuedMap

The successor of MultiMap is the org.apache.commons.collections4.MultiValuedMap interface. It has multiple implementations ready to be used.

MultiMap的后继者是org.apache.commons.collection4.MultiValuedMap接口。它有多种实现可供使用。

Let’s see how to store our multiple values into an ArrayList, which retains duplicates:

让我们看看如何将我们的多个值存储到一个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");

Alternatively, we could use a HashSet, which drops duplicates:

另外,我们可以使用一个HashSet,它可以丢弃重复的内容。

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

Both of the above implementations are not thread-safe.

上述两种实现方式都不是线程安全的。

Let’s see how we can use the UnmodifiableMultiValuedMap decorator to make them immutable:

让我们看看如何使用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

5 Guava Multimap

Guava is the Google Core Libraries for Java API.

Guava是谷歌核心库的Java API。

Let’s start by importing Guava on our project:

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

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

Guava followed the path of multiple implementations since the beginning.

Guava从一开始就走的是多种实现的道路。

The most common one is the com.google.common.collect.ArrayListMultimap, which uses a HashMap backed by an ArrayList for every value:

最常见的是com.google.com.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");

As always, we should prefer the immutable implementations of the Multimap interface: com.google.common.collect.ImmutableListMultimap and com.google.common.collect.ImmutableSetMultimap.

一如既往,我们应该更喜欢Multimap接口的不可变的实现。com.google.com.collect.ImmutableListMultimapcom.google.com.collect.ImmutableSetMultimap

5.1. Common Map Implementations

5.1.常见的地图实现

When we need a specific Map implementation, the first thing to do is check if it exists because Guava has probably already implemented it.

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

For example, we can use the com.google.common.collect.LinkedHashMultimap, which preserves the insertion order of keys and values:

例如,我们可以使用com.google.com.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");

Alternatively, we can use a com.google.common.collect.TreeMultimap, which iterates keys and values in their natural order:

另外,我们可以使用com.google.com.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. Forging Our Custom MultiMap

5.2.锻造我们的自定义MultiMap

Many other implementations are available.

还有许多其他的实现方式。

However, we may want to decorate a Map and/or a List that’s not yet implemented.

然而,我们可能想装饰一个Map和/或一个尚未实现的List

Luckily, Guava has a factory method allowing us to do it — the Multimap.newMultimap().

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

6. Conclusion

6.结论

We’ve seen how to store multiple values for a key in a Map in all the main existing ways.

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

We’ve explored the most popular implementations of Apache Commons Collections and Guava, which should be preferred to custom solutions when possible.

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

As always, the full source code is available over on GitHub.

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