1. 概述
在这个教程中,我们将探讨Java中除了常见Collection
类之外的两种集合类型。众所周知,我们有三个核心集合类:Map
、List
和Set
。它们各自有对应的不可变和只读版本。
在我们的示例中,我们将关注Java中的Map
家族集合。Collections.unmodifiableMap()
和Map.of()
方法适用于Map
,而Collections.unmodifiableList()
、Collections.unmodifiableSet()
、List.of()
和Set.of()
是List
和Set
集合类的相应实用方法。List
和Set
集合类同样适用这些概念。
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"));
modifiableMap
和unModifiableMap
引用指向内存中的同一张地图,但它们的行为不同。一个可以自由操作地图,而另一个不能通过添加或删除项目来修改集合。
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. 总结
在这篇文章中,我们详细研究了Map
、List
和Set
等集合类的不可变和只读版本。当需要通过特定引用保持集合不变,但又希望原始集合仍可变时,不可变集合是合适的。另一方面,当完全不想让集合受到任何修改,即使通过任何引用,不可变集合是最理想的。我们还讨论了一些常见用例。如往常一样,本文的源代码可以在GitHub上找到。