不可变集合与不可修改集合
Posted
技术标签:
【中文标题】不可变集合与不可修改集合【英文标题】:Immutable vs Unmodifiable collection 【发布时间】:2012-02-12 02:56:33 【问题描述】:来自Collections Framework Overview:
不支持修改操作的集合(例如
add
、remove
和clear
)被称为不可修改。不可修改的集合是可修改的。另外保证
Collection
对象中没有任何变化的集合被称为不可变。不可变的集合是可变的。
我无法理解其中的区别。unmodifiable 和 immutable 这里有什么区别?
【问题讨论】:
【参考方案1】:不可修改与不可变集合
创建可修改的地图
Map<String, String> modifiableMap = new HashMap();
modifiableMap.put(“1”,”one”);
modifiableMap.put(“2”,”two”);
modifiableMap.put(“3”,”three”);
从 modifiableMap 中创建一个 unmodifiableMap
Map<String,String> unmodifiableMap = Collections.unmodifiableMap(modifiableMap);
unmodifiableMap.put(“4”,”Four”) ==>Exception
modifiableMap.put(“4”,”Four”); ==>Allowed, this will also reflect now in the unmodifiableMap , because unmodifiableMap() returns a wrapper around modifiableMap.
从 modifiableMap 创建一个 immutableMap
Map<String,String> immutableMap = Collections.immutableMap(modifiableMap);
immutableMap.put(“5”,”Five”) ==>Exception
modifiableMap.put(“5”,”Five”); ==>Allowed, BUT this will NOT reflect now in the immutableMap, because immutableMap() returns a copy of the modifiableMap.
【讨论】:
【参考方案2】:如果一个对象在构造后它的状态不能改变,那么它就被认为是不可变的。在你创建了一个不可变的集合实例之后,只要对它的引用存在,它就会持有相同的数据。
不可变集合的一个优点是它自动是线程安全的。 包含不可变对象的集合在构造后自动成为线程安全的。创建这样的集合后,可以将其交给多个线程,它们都会看到一致的视图。
但是,不可变对象的集合与不可变对象的集合不同。如果包含的元素是可变的,那么这可能会导致集合的行为不一致或使其内容出现改变。
简单来说,如果你给可变的东西添加一点不变性,你就会得到可变性。如果你给不可变的东西添加一点可变性,你就会得到可变性。
不可变和不可修改是不一样的:
不可变集合的行为方式与 Collections.unmodifiable... 包装器相同。但是,这些集合不是包装器——它们是由类实现的数据结构,任何修改数据的尝试都会引发异常。
如果您创建一个 List 并将其传递给 Collections.unmodifiableList 方法,那么您将获得一个不可修改的视图。底层列表仍然是可修改的,对它的修改通过返回的 List 可见,因此它实际上不是不可变的。
要演示此行为,请创建一个列表并将其传递给 Collections.unmodifiableList。如果您尝试直接添加到该不可修改的列表中,则会引发 UnsupportedOperationException。
但是,如果你改变了原来的List,不会产生错误,并且修改了不可修改的List。
在这种情况下,要使集合在构建后不可变,最好不要维护对支持集合的引用。这绝对保证了不变性。
此外,允许某些客户端以只读方式访问您的数据结构。您可以保留对支持集合的引用,但分发对包装器的引用。这样,客户可以查看但不能修改,同时您保持完全访问权限。
因此,不可变集合可以包含可变对象,如果包含,则该集合既不是不可变的,也不是线程安全的。
【讨论】:
【参考方案3】: // normal list
List list1 = new ArrayList();
list1.add(1);
// unmodifiable list
List list2 = Collections.unmodifiableList(list1);
// immutable list
List list3 = Collections.unmodifiableList(new ArrayList<>(list1));
list1.add(2);
list1.add(3);
System.out.println(list1);
System.out.println(list2);
System.out.println(list3);
输出:
[1, 2, 3]
[1, 2, 3]
[1]
【讨论】:
【参考方案4】:[Unmodifiable and Immutable]
仍然可以通过更改源对象来更改不可修改的集合(对象)。可以使用参考。
Java 提供了几种创建不可修改地图的方法:
Collections.unmodifiableMap()
Java 9 Map.of()
, Map.ofEntries()
【讨论】:
【参考方案5】:如果我们在谈论 JDK Unmodifiable*
与 guava Immutable*
,实际上差异还在于 性能。如果不可变集合不是围绕常规集合的包装器(JDK 实现是包装器),则它们可以更快、更节省内存。
Citing the guava team:
JDK 提供了 Collections.unmodifiableXXX 方法,但在我们看来,这些都可以
<...>
低效:数据结构仍然具有可变集合的所有开销,包括并发修改检查、哈希表中的额外空间等。【讨论】:
考虑到性能,您还应该考虑到不可修改的包装器不会复制集合,其中在 guava 中使用不可变版本,现在也在 jdk9+ 中使用例如List.of(...)
确实复制了两次!
@benez 你有这个链接。
@alfonx 我不确定您期望哪些链接。有不同的jdk。例如来自github【参考方案6】:
Java™ 教程如下:
与同步包装器不同,后者将功能添加到 包装的集合,不可修改的包装器会带走功能。 特别是,它们剥夺了通过以下方式修改集合的能力 拦截所有会修改集合的操作和 抛出 UnsupportedOperationException。不可修改的包装器有 两个主要用途,如下:
使集合一旦构建就不可变。在这种情况下, 最好不要保留对支持的引用 收藏。这绝对保证了不变性。
允许某些客户端以只读方式访问您的数据结构。你 保留对支持集合的引用,但分发对 包装器。这样,客户可以查看但不能修改,而您 保持完全访问权限。
我认为这是一个足以理解差异的解释。
【讨论】:
【参考方案7】:引用The Java™ Tutorials:
与将功能添加到已包装集合的同步包装器不同,不可修改的包装器会带走功能。特别是,它们通过拦截所有会修改集合的操作并抛出 UnsupportedOperationException 来取消修改集合的能力。不可修改的包装器有两个主要用途,如下:
使集合在构建后不可变。在这种情况下,最好不要维护对支持集合的引用。这绝对保证了不变性。
允许某些客户端只读访问您的数据结构。您保留对支持集合的引用,但分发对包装器的引用。这样,客户可以查看但不能修改,同时您保持完全访问权限。
(强调我的)
这确实总结了它。
【讨论】:
【参考方案8】:如上所述,不可修改的不像不可变的,因为不可修改的集合可以被更改,例如,如果不可修改的集合有一个底层委托集合,该集合被其他对象引用并且该对象改变了它。
关于不可变,它甚至没有很好的定义。但是,通常这意味着对象“不会改变”,但这需要递归定义。例如,我可以在其实例变量都是基元且其方法均不包含参数并返回基元的类上定义不可变。然后,这些方法递归地允许实例变量是不可变的,并且所有方法都包含不可变的参数并返回不可变的值。应该保证这些方法随着时间的推移返回相同的值。
假设我们可以做到这一点,还有线程安全的概念。 您可能会认为不可变(或不随时间变化)也意味着线程安全。 但事实并非如此,这就是我要说明的要点这里尚未在其他答案中注明。我可以构造一个始终返回相同结果但不是线程安全的不可变对象。要看到这一点,假设我通过随着时间的推移维护添加和删除来构造一个不可变集合。现在,不可变集合通过查看内部集合(可能会随着时间而变化)然后(在内部)添加和删除在创建集合后添加或删除的元素来返回其元素。显然,虽然集合总是返回相同的元素,但它不是线程安全的,因为它永远不会改变值。
现在我们可以将不可变对象定义为线程安全且永远不会更改的对象。有一些创建不可变类的准则通常会导致此类类,但是请记住,可能有一些方法可以创建不可变类,这需要注意线程安全,例如,如上面的“快照”集合示例中所述。
【讨论】:
【参考方案9】:基本上unModifiable
Collection 是一个视图,因此间接地它仍然可以从其他可修改的参考中“修改”。此外,由于它只是另一个集合的只读视图,当源集合更改时,不可修改的集合将始终显示最新值。
但是immutable
集合可以被视为另一个集合的只读副本并且不能被修改。在这种情况下,当源集合发生变化时,不可变集合不会反映变化
这是一个可视化这种差异的测试用例。
@Test
public void testList()
List<String> modifiableList = new ArrayList<String>();
modifiableList.add("a");
System.out.println("modifiableList:"+modifiableList);
System.out.println("--");
//unModifiableList
assertEquals(1, modifiableList.size());
List<String> unModifiableList=Collections.unmodifiableList(
modifiableList);
modifiableList.add("b");
boolean exceptionThrown=false;
try
unModifiableList.add("b");
fail("add supported for unModifiableList!!");
catch (UnsupportedOperationException e)
exceptionThrown=true;
System.out.println("unModifiableList.add() not supported");
assertTrue(exceptionThrown);
System.out.println("modifiableList:"+modifiableList);
System.out.println("unModifiableList:"+unModifiableList);
assertEquals(2, modifiableList.size());
assertEquals(2, unModifiableList.size());
System.out.println("--");
//immutableList
List<String> immutableList=Collections.unmodifiableList(
new ArrayList<String>(modifiableList));
modifiableList.add("c");
exceptionThrown=false;
try
immutableList.add("c");
fail("add supported for immutableList!!");
catch (UnsupportedOperationException e)
exceptionThrown=true;
System.out.println("immutableList.add() not supported");
assertTrue(exceptionThrown);
System.out.println("modifiableList:"+modifiableList);
System.out.println("unModifiableList:"+unModifiableList);
System.out.println("immutableList:"+immutableList);
System.out.println("--");
assertEquals(3, modifiableList.size());
assertEquals(3, unModifiableList.size());
assertEquals(2, immutableList.size());
输出
modifiableList:[a]
--
unModifiableList.add() not supported
modifiableList:[a, b]
unModifiableList:[a, b]
--
immutableList.add() not supported
modifiableList:[a, b, c]
unModifiableList:[a, b, c]
immutableList:[a, b]
--
【讨论】:
我看不出有什么区别,你能指出 Immutable 有什么不同吗?我可以看到不可变和不可修改都抛出错误并且不支持添加。我在这里错过了什么吗? @AKS 请在将“c”添加到列表后查看最后三个列表条目的输出,而modifiableList
和 unModifiableList
的大小都增加了 immutableList
大小没有改变
哦!知道了! :).. 所以这里你使用 modifiableList 中的更改修改了 unmodifableList,但是 ImmutableList 不能被修改。但是同样的方式你也可以修改 ImmutableList,我认为这里的客户端将只能访问 ImmutableList 引用,对 modifiableList 的引用,使用它创建 ImmutableList,不会暴露给客户端。对吗?
是的,因为没有引用new ArrayList<String>(modifiableList)
immutableList 不能修改
使集合一旦构建就成为不可变的。在这种情况下,最好不要维护对支持集合的引用。这绝对保证了不变性。这就是第三个列表中正在做的事情。很好的编码示例。谢谢..【参考方案10】:
我认为主要区别在于可变集合的所有者可能希望向其他代码提供对该集合的访问权限,但通过不允许其他代码修改集合的接口提供该访问权限(而将该功能保留给拥有的代码)。所以集合不是不可变的,但某些用户不允许更改集合。
Oracle 的Java Collection Wrapper tutorial 有这样的说法(强调添加):
不可修改的包装器有两个主要用途,如下:
使集合在构建后不可变。在这种情况下,最好不要保留对支持的引用 收藏。这绝对保证了不变性。 允许某些客户以只读方式访问您的数据结构。您保留对支持集合的引用,但手 出对包装器的引用。这样,客户可以看但看不到 修改,同时您保持完全访问权限。
【讨论】:
【参考方案11】:一个不可修改的集合通常是一个可修改集合的包装器其他代码可能仍然可以访问。因此,如果您只有对不可修改集合的引用,则您无法对其进行任何更改,但您不能依赖于内容不变。
不可变 集合保证 nothing 可以再改变集合。如果它包装了一个可修改的集合,它确保没有其他代码可以访问该可修改的集合。请注意,尽管没有代码可以更改集合包含引用的对象,但对象本身可能仍然是可变的 - 创建 StringBuilder
的不可变集合不会以某种方式“冻结”这些对象。
基本上,区别在于其他代码是否能够改变你背后的集合。
【讨论】:
不可变集合并不能保证 nothing 可以再改变。它只是确保集合本身不能被更改(不是通过包装,而是通过复制)。集合中存在的对象仍然可以更改,并且不提供任何保证。 @HieryNomus:请注意,我并没有说什么都可以改变 - 我说什么都不能改变集合。 好的,可能误读了 ;) 但最好澄清一下。 所以,你在说什么。为了真正的不变性,您需要一个包含不可变类型项目的不可变集合。 @savanibharat:这取决于是否有任何代码路径仍然可以修改list
。如果以后可以调用list.add(10)
,那么coll
将反映这种变化,所以不,我不会称它为不可变的。以上是关于不可变集合与不可修改集合的主要内容,如果未能解决你的问题,请参考以下文章