1. 概述

在这个教程中,我们将探讨Java中除了常见Collection类之外的两种集合类型。众所周知,我们有三个核心集合类:MapListSet。它们各自有对应的不可变和只读版本。

在我们的示例中,我们将关注Java中的Map家族集合。Collections.unmodifiableMap()Map.of()方法适用于Map,而Collections.unmodifiableList()Collections.unmodifiableSet()List.of()Set.of()ListSet集合类的相应实用方法。ListSet集合类同样适用这些概念。

2. 不可变和只读集合

不可变集合是围绕可变集合创建的一个包装器,通过包装引用阻止对内部集合进行修改。例如,在Java Map集合中,我们可以使用unmodifiableMap()实用方法获取不可变引用:

Map<String, String> modifiableMap = new HashMap<>();
modifiableMap.put("name1", "Michael");
modifiableMap.put("name2", "Harry"

这里,modifiableMap是一个指向地图的引用。我们在地图中添加两个键值对。接下来,我们使用Collection.unmodifiableMap()方法获取unmodifiableMap

Map<String, String> unmodifiableMap = Collections.unmodifiableMap(modifiableMap);

我们得到了一个新的引用unmodifiableMap,指向底层集合。这个不可变引用的特殊之处在于,我们不能通过它向地图中添加或删除条目。但它不影响底层集合或其他引用变量modifiableMap。我们仍然可以使用初始modifiableMap引用向集合中添加更多键值对:

modifiableMap.put("name3", "Micky");

对集合的更改也会反映在新的引用变量unmodifiableMap上:

assertEquals(modifiableMap, unmodifiableMap);
assertTrue(unmodifiableMap.containsKey("name3"));

现在尝试使用unmodifiableMap引用变量插入一个条目,预期会阻止操作并抛出异常:

assertThrows(UnsupportedOperationException.class, () -> unmodifiableMap.put("name3", "Micky"));

modifiableMapunModifiableMap引用指向内存中的同一张地图,但它们的行为不同。一个可以自由操作地图,而另一个不能通过添加或删除项目来修改集合。

3. 不可变集合

不可变集合在其生命周期内始终保持不变,没有任何可修改的引用。不可变集合解决了我们能够使用其他引用修改不可变集合的问题。在Java中,我们使用Map.of()List.of()等实用方法创建不可变集合。任何新创建的引用也将始终是不可变的:

@Test
public void givenImmutableMap_WhenPutNewEntry_ThenThrowsUnsupportedOperationException() {
    Map<String, String> immutableMap = Map.of("name1", "Michael", "name2", "Harry");

    assertThrows(UnsupportedOperationException.class, () -> immutableMap.put("name3", "Micky"));
}

当我们尝试将条目放入immutableMap时,会立即遇到UnsupportedOperationException异常。Map.copyOf()方法返回一个指向底层不可变地图的引用:

@Test
public void givenImmutableMap_WhenUsecopyOf_ThenExceptionOnPut() {
    Map<String, String> immutableMap = Map.of("name1", "Michael", "name2", "Harry");
    Map<String, String> copyOfImmutableMap = Map.copyOf(immutableMap);

    assertThrows(UnsupportedOperationException.class, () -> copyOfImmutableMap.put("name3", "Micky"));
}

因此,如果我们希望确保没有其他引用可以修改集合,我们应该在Java中选择不可变集合。

4. 不可变和只读集合的考虑

4.1. 线程安全

不可变类本质上是线程安全的,因为多个线程可以同时访问它们,而不用担心改变底层集合。使用不可变集合可以防止多个线程覆盖状态,从而实现线程安全的设计。线程安全意味着在并发环境中使用时不需要显式同步。这简化了并发编程,消除了对锁等的需求。

4.2. 性能

与相应的可变集合相比,不可变或只读集合的性能较差。更新时无法进行就地更新,而是必须创建对象的新副本,这增加了开销,导致性能不佳。此外,由于需要创建新实例,它们可能比可变版本占用更多的内存。然而,在频繁读取和偶尔写入的场景中,不可变集合表现优秀。

4.3. 可变对象

在将可变对象添加到不可变集合时,我们必须确保对其进行防御性复制,以防止外部修改。在多线程上下文中,需要确保集合和其中包含的可变对象都具有线程安全性。

5. 总结

在这篇文章中,我们详细研究了MapListSet等集合类的不可变和只读版本。当需要通过特定引用保持集合不变,但又希望原始集合仍可变时,不可变集合是合适的。另一方面,当完全不想让集合受到任何修改,即使通过任何引用,不可变集合是最理想的。我们还讨论了一些常见用例。如往常一样,本文的源代码可以在GitHub上找到。