1.概述

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

2.标准Map

Java有几个接口Map的实现,每个都有自己的特殊性。

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

我们可以看到,如果我们尝试为同一个键插入两个值,则将存储第二个值,而第一个值将被删除。

它也将被返回(通过*put(K键,V值)*方法的每个正确实现):

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.集合作为Value

显然,对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 多重映射

该*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.collections4。MultiValuedMap*接口。它有多个可以使用的实现。

让我们看看如何将多个值存储到ArrayList中,该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的Google核心库。

该*com.google.common.collect。从版本2开始,Multimap界面就出现了。在撰写本文时,最新版本是25,但是自从版本23之后它被分割为jreandroid*(25.0-jre25.0-android)的不同分支,我们仍然会使用我们的例子版本23。

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


<dependency>

<groupId>com.google.guava</groupId>

<artifactId>guava</artifactId>

<version>23.0</version>

</dependency>

Guava从一开始就遵循多个实现的路径。

最常见的是*com.google.common.collect。ArrayListMultimap,它为每个值使用由ArrayList支持的HashMap*:


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.common.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最流行的实现,如果可能的话,它应该优先于自定义解决方案。

原文链接
https://mp.weixin.qq.com/s/lcUKd-AMXGPix7CEplRmkg

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐