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 获取。