1. 概述

在某些场景下,我们希望禁止对 java.util.Map 的修改操作,比如在多线程环境中共享只读数据。为了实现这一点,我们可以选择使用 不可修改的 Map(Unmodifiable Map)不可变 Map(Immutable Map)

本文将快速带你了解它们之间的区别,并介绍几种创建不可变 Map 的方式。

2. 不可修改 vs 不可变

✅ 不可修改的 Map(Unmodifiable Map)

不可修改的 Map 只是对一个可修改 Map 的包装,它本身并不持有数据,只是阻止了外部对它的直接修改操作:

Map<String, String> mutableMap = new HashMap<>();
mutableMap.put("USA", "North America");

Map<String, String> unmodifiableMap = Collections.unmodifiableMap(mutableMap);
assertThrows(UnsupportedOperationException.class,
  () -> unmodifiableMap.put("Canada", "North America"));

⚠️ 但它底层的原始 Map 仍然是可变的,如果原始 Map 被修改,这些变化也会反映在不可修改 Map 中:

mutableMap.remove("USA");
assertFalse(unmodifiableMap.containsKey("USA"));
        
mutableMap.put("Mexico", "North America");
assertTrue(unmodifiableMap.containsKey("Mexico"));

✅ 不可变 Map(Immutable Map)

相比之下,不可变 Map 拥有自身的私有数据,一旦创建就不能以任何形式进行修改。

3. 使用 Guava 的 ImmutableMap

Guava 提供了 ImmutableMap 类,它是对 java.util.Map 的不可变实现。任何尝试修改它的操作都会抛出 UnsupportedOperationException

由于其内部数据是私有的,即使原始 Map 发生变化,也不会影响已创建的 ImmutableMap 实例。

下面我们将介绍几种创建 ImmutableMap 实例的方式。

3.1. 使用 copyOf() 方法

通过 ImmutableMap.copyOf() 方法,我们可以创建一个包含原始 Map 所有条目的副本:

ImmutableMap<String, String> immutableMap = ImmutableMap.copyOf(mutableMap);
assertTrue(immutableMap.containsKey("USA"));

这个副本既不能被直接修改,也不会受到原始 Map 变化的影响:

assertThrows(UnsupportedOperationException.class,
  () -> immutableMap.put("Canada", "North America"));
        
mutableMap.remove("USA");
assertTrue(immutableMap.containsKey("USA"));
        
mutableMap.put("Mexico", "North America");
assertFalse(immutableMap.containsKey("Mexico"));

3.2. 使用 builder() 方法

我们也可以使用 ImmutableMap.builder() 来构建一个不可变 Map,它支持链式调用,并允许添加额外的键值对:

ImmutableMap<String, String> immutableMap = ImmutableMap.<String, String>builder()
  .putAll(mutableMap)
  .put("Costa Rica", "North America")
  .build();
assertTrue(immutableMap.containsKey("USA"));
assertTrue(immutableMap.containsKey("Costa Rica"));

和前面一样,这个 Map 不能被修改,也不会受到原始 Map 的影响:

assertThrows(UnsupportedOperationException.class,
  () -> immutableMap.put("Canada", "North America"));
        
mutableMap.remove("USA");
assertTrue(immutableMap.containsKey("USA"));
        
mutableMap.put("Mexico", "North America");
assertFalse(immutableMap.containsKey("Mexico"));

3.3. 使用 of() 方法

最后,我们可以使用 ImmutableMap.of() 方法快速创建一个最多包含 5 个键值对 的不可变 Map:

ImmutableMap<String, String> immutableMap
  = ImmutableMap.of("USA", "North America", "Costa Rica", "North America");
assertTrue(immutableMap.containsKey("USA"));
assertTrue(immutableMap.containsKey("Costa Rica"));

当然,它也不支持任何修改操作:

assertThrows(UnsupportedOperationException.class,
  () -> immutableMap.put("Canada", "North America"));

3.4. 使用 ofEntries() 方法

对于需要创建更多键值对的场景,我们可以使用 ImmutableMap.ofEntries() 方法。这个方法支持传入任意数量的键值对(以 Map.Entry 形式)。

ImmutableMap<Integer, String> immutableMap
  = ImmutableMap.ofEntries(new AbstractMap.SimpleEntry<>(1, "USA"));
assertEquals(1, immutableMap.size());
assertThat(immutableMap, IsMapContaining.hasEntry(1, "USA"));

这里我们使用了 AbstractMap.SimpleEntry 来构造键值对。

尝试修改返回的 Map 会抛出异常:

ImmutableMap<Integer, String> immutableMap
  = ImmutableMap.ofEntries(new AbstractMap.SimpleEntry<>(1, "USA"), new AbstractMap.SimpleEntry<>(2, "Canada"));
assertThrows(UnsupportedOperationException.class, () -> immutableMap.put(2, "Mexico"));

此外,如果传入了重复的 key,会抛出 IllegalArgumentException

assertThrows(IllegalArgumentException.class,
  () -> ImmutableMap.ofEntries(new AbstractMap.SimpleEntry<>(1, "USA"), new AbstractMap.SimpleEntry<>(1, "Canada")));

⚠️ 注意:该方法不允许 key 或 value 为 null,否则会抛出 NullPointerException

assertThrows(NullPointerException.class,
  () -> ImmutableMap.ofEntries(new AbstractMap.SimpleEntry<>(null, "USA")));

assertThrows(NullPointerException.class,
  () -> ImmutableMap.ofEntries(new AbstractMap.SimpleEntry<>(1, null)));

4. 总结

本文简单对比了 不可修改 Map不可变 Map 的区别,并介绍了使用 Guava 创建 ImmutableMap 的几种常见方式。

这些不可变集合在并发编程、函数式编程、缓存等场景中非常有用,值得熟练掌握。

✅ 完整示例代码可在 GitHub 获取。


原始标题:Immutable Map Implementations in Java