不可变集合 vs 不可修改集合

213
集合框架概述中得知:
不支持修改操作(如addremoveclear)的集合被称为不可修改的(unmodifiable)。不是不可修改的都是可修改的(modifiable)。
此外,保证Collection对象不会显示更改的集合被称为不可变的(immutable)。不是不可变的都是可变的(mutable)。
我不能理解这个区别。这里不可修改的不可变的有什么区别?
11个回答

246

一个不可修改(unmodifiable)的集合通常是对一个可修改(modifiable)集合的包装,而其他代码仍然可以访问该可修改集合。因此,如果您只有对不可修改集合的引用,则无法对其进行任何更改,但是不能保证其内容不会更改。

一个不可变(immutable)的集合保证了没有任何东西可以再改变它。如果它是对可修改集合的包装,它会确保其他任何代码都无法访问该可修改集合。请注意,尽管没有任何代码可以更改集合所包含对象的引用,但这些对象本身仍然可能是可变的——创建一个由StringBuilder组成的不可变集合并不会“冻结”这些对象。

基本上,区别在于其他代码是否可能悄悄地更改了集合。


77
不可变集合并不保证什么都不能改变了。它只是确保集合本身无法被更改(而不是通过包装,而是通过复制)。存在于集合中的对象仍然可以被更改,并且对这些对象没有任何保证。 - Hiery Nomus
8
请注意,我并没有说任何事情都不可能改变 - 我说的是没有任何事情可以改变这个收藏品。 - Jon Skeet
1
好的,可能是我理解有误;)但澄清一下还是很好的。 - Hiery Nomus
5
你的意思是,为了真正实现不可变性,你需要一个包含不可变类型项的不可变集合。 - Evan Plaice
1
@savanibharat:这取决于是否有任何代码路径仍然可以修改“list”。如果稍后有东西调用“list.add(10)”,那么“coll”将反映出该更改,因此,不,我不会称其为不可变的。 - Jon Skeet
显示剩余4条评论

122

基本上,unModifiable集合是一个视图,因此可以通过其他可修改的引用间接地对其进行“修改”。 同时,它只是另一个集合的只读视图,当源集合更改时,不可修改的集合始终呈现最新的值。

然而,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有何不同吗?我发现Immutable和unmodifiable都会抛出错误,而且不支持添加操作。我是不是漏掉了什么? - AKS
3
请注意,在将“c”添加到列表后,最后三个列表条目的输出情况,尽管modifiableListunModifiableList的大小均已增加,但immutableList的大小未发生变化。请参考。 - Prashant Bhate
1
哦!明白了! :).. 所以在这里,您使用可修改列表中的更改来修改不可修改列表,但是ImmutableList无法被修改。但是同样的方式,您也可以修改ImmutableList,我认为客户端只能访问ImmutableList引用,创建ImmutableList的可修改列表的引用将不会暴露给客户端。对吗? - AKS
2
是的,因为没有引用new ArrayList<String>(modifiableList)不可变列表无法被修改。 - Prashant Bhate
@PrashantBhate 你好,没有提到new ArrayList<String>(modifiableList)是因为有new吗?谢谢。 - Unheilig
1
将集合构建完成后变为不可变。在这种情况下,最好不要保留对支持集合的引用。这绝对保证了不可变性。这就是第三个列表中所做的。良好的编码示例。谢谢。 - Bharath

15

我认为主要区别在于可变集合的所有者可能希望向其他代码提供对集合的访问权限,但通过一个接口提供这个访问权限,使得其他代码不能修改集合(而保留该功能给拥有代码)。所以集合不是不可变的,但某些用户不被允许更改集合。

Oracle的Java Collection Wrapper教程说道(强调添加):

Unmodifiable包装器有以下两个主要用途:

  • 一旦构建完成,使集合变为不可变。在这种情况下,最好不保留对支持集合的引用,这绝对保证了不可变性。
  • 允许某些客户端只读访问您的数据结构。您保留对支持集合的引用,但交出对包装器的引用。通过这种方式,客户端可以查看但不能修改,而您仍然保持完全访问权限

5

如果一个对象在构造后其状态无法更改,那么它被认为是不可变的。创建不可变集合实例后,只要存在对它的引用,它就会持有相同的数据。

不可变集合的一个优点是它自动具备线程安全性。包含不可变对象的集合在构建后自动具备线程安全性。创建这样的集合后,您可以将其交给多个线程,它们将看到一致的视图。

然而,一个对象不可变的集合和一个不可变对象的集合并不是一回事。如果包含的元素是可变的,则可能导致集合行为不一致或导致其内容似乎发生了变化。

简单来说,如果你把一些不可变性加到可变的东西中,你就得到了可变性。如果你把一些可变性加到不可变的东西中,你就得到了可变性。

不可变和不可修改不是一回事:

不可变集合的行为方式与Collections.unmodifiable...封装器相同。但是,这些集合不是封装器——它们是由类实现的数据结构,在尝试修改数据时会引发异常。

如果创建一个List并将其传递给Collections.unmodifiableList方法,则会得到一个不可修改的视图。底层列表仍然是可修改的,并且对它的修改可以通过返回的List看到,因此它实际上并不是不可变的。

为了展示这种行为,创建一个List并将其传递给Collections.unmodifiableList。如果您尝试直接向该不可修改的List中添加元素,则会引发UnsupportedOperationException异常。

但是,如果更改原始列表,则不会生成错误,并且不可修改的List已被修改。

在这种情况下,要使集合在构建后不可变,最好不要保留对支撑集合的引用。这绝对保证了不可变性。

此外,为了允许某些客户端只读访问您的数据结构。您可以保留对支持集合的引用,但是分发对包装器的引用。这样,客户端可以查看但无法修改,而您仍然保持完全访问权限。

因此,一个不可变集合可以包含可变对象,如果存在可变对象,则该集合既不是不可变的也不具备线程安全性。


4
如果我们谈论JDK的Unmodifiable*与guava的Immutable*,实际上它们之间的区别也在于性能。如果不是普通集合的包装器(JDK实现是包装器),那么Immutable集合可以更快且更节省内存。引用guava团队的话:(点击查看)
JDK提供了Collections.unmodifiableXXX方法,但在我们看来,这些方法可能会导致以下问题:
  • 效率低下:数据结构仍然具有可变集合的所有开销,包括并发修改检查、哈希表中的额外空间等。

1
在考虑性能时,您还应该考虑到一个不可修改的包装器不会复制集合,而使用Guava中的不可变版本以及现在在JDK9+中也是如此,例如List.of(...)确实会复制两次! - benez
@benez,请问你有相关链接吗? - alfonx
@alfonx 我不确定你期望哪些链接。有不同的JDK。例如来自github - benez

4
引用自Java™教程的一段话:

与同步包装器不同,它们增加了封装集合的功能,而不可修改的包装器则去掉了功能。特别地,它们通过拦截所有会改变集合的操作并抛出UnsupportedOperationException来取走修改集合的能力。不可修改的包装器有两个主要用途:

  • 在建立好一个集合之后使其保持不可变性。在这种情况下,最好不要维护对后备集合的引用。这样可以完全保证不可变性。

  • 允许某些客户只读访问数据结构。您保留对后备集合的引用,但分配一个对包装器的引用。以这种方式,客户端可以查看但无法修改,而您则保持完全访问权限。

(强调已添加)

这真的很概括。

4
    // 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]

3

不可修改 vs 不可变集合

创建一个可修改的映射表

Map<String, String> modifiableMap = new HashMap();
modifiableMap.put(“1”,”one”);
modifiableMap.put(“2”,”two”);
modifiableMap.put(“3”,”three”);

将可修改的Map转换为不可修改的Map

 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.
     

从可修改的映射表中创建不可变的映射表。
 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.

1
Java™教程中解释如下:
与同步包装器不同,后者为包装的集合添加功能,而不可修改的包装器则取走了功能。特别是,它们拦截所有修改集合的操作,并抛出UnsupportedOperationException异常,从而剥夺了修改集合的能力。 不可修改的包装器有两个主要用途,如下所示:
1.建立一次性集合后使其不可变。在这种情况下,最好不要保持对支持集合的引用,这绝对保证了不可变性。
2.允许某些客户端只读访问数据结构。您保留对支持集合的引用,但分配包装器的引用。通过这种方式,客户端可以查看但不能修改,而您可以保持完全访问权限。
我认为这已经很好地解释了差异。

1

正如上面所提到的,unmodifiable(不可修改)与immutable(不可变)不同,因为一个unmodifiable集合可以被更改,例如,如果一个unmodifiable集合具有一个底层委托集合,该集合由其他对象引用并且那个对象更改了它。

关于immutable,甚至都没有很好地定义。但是,通常意味着对象“不会改变”,但需要递归地定义。例如,我可以在所有实例变量都是原始类型并且方法都不含参数且返回原始类型时的类中定义immutable。然后,这些方法递归允许实例变量是immutable的,并且所有方法都包含是immutable的参数和返回immutable值。这些方法应保证随着时间的推移返回相同的值。

假设我们能够这样做,还有一个概念是线程安全。你可能会认为不可变对象(或随时间不可更改的对象)也意味着线程安全。然而,情况并非如此,这是我在这里提出的主要观点,其他答案中尚未注意到。我可以构造一个始终返回相同结果但不是线程安全的不可变对象。为了看清楚这一点,假设我通过随时间维护添加和删除来构建一个不可变集合。现在,该不可变集合通过查看内部集合(可能随时间而变化)返回其元素,然后在创建集合之后(内部)添加和删除已添加或删除的元素。显然,尽管集合始终返回相同的元素,但它并不是线程安全的,仅仅因为它永远不会改变值。
现在,我们可以将不可变定义为线程安全且永远不会更改的对象。有关创建通常导致此类类的不可变类的指南,但请记住,可能存在需要注意线程安全的创建不可变类的方法,例如上面描述的“快照”集合示例。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接