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.ImmutableListMultimap 及 com.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 上获取。