为什么我的 `unmodifiableList` 是可修改的?

Why is my `unmodifiableList` modifiable?

我想要一个List,其元素不能被删除或添加。我以为我在 Java 8 中找到了 Collections.unmodifiableList 的答案。我传递了我的原始列表并取回了一个据称不可修改的列表。

然而,当我从原始列表中删除一个元素时,我的不可修改列表被修改了。到底是怎么回事?

查看此演示代码。从原始列表中删除时,我的不可修改列表从 3 个元素 2 缩减。

String dog = "dog";
String cat = "cat";
String bird = "bird";

List< String > originalList = new ArrayList<>( 3 );
originalList.add( dog );
originalList.add( cat );
originalList.add( bird );

List< String > unmodList = Collections.unmodifiableList( originalList );
System.out.println( "unmod before: " + unmodList );  // Yields [dog, cat, bird]
originalList.remove( cat );  // Removing element from original list affects the unmodifiable list?
System.out.println( "unmod after: " + unmodList );  // Yields [dog, bird]

原始列表支持 unmodifiableList

那个 unmodifiableList method in Collections 实用程序 class 不会创建一个新列表,它会创建一个由原始列表 支持 的伪列表。通过 "unmodifiable" 对象进行的任何添加或删除尝试都将被阻止,因此该名称符合其用途。但实际上,正如您所展示的那样,可以修改原始列表并同时影响我们的次要列表。

这在 class 文档中有详细说明:

Returns an unmodifiable view of the specified list. This method allows modules to provide users with "read-only" access to internal lists. Query operations on the returned list "read through" to the specified list, and attempts to modify the returned list, whether direct or via its iterator, result in an UnsupportedOperationException.

第四个词是关键:view。新列表对象不是新列表。它是一个叠加层。就像 tracing paper or transparency film 在绘图上阻止您在绘图上做标记一样,它不会阻止您在下面修改原始绘图。

故事的寓意:不要使用 Collections.unmodifiableList 制作列表的防御性副本。

同上 Collections.unmodifiableMap, Collections.unmodifiableSet,依此类推。

Google番石榴

对于防御性编程,我建议使用 Google Guava library and its ImmutableCollections 设施而不是 Collections class。

您可以创建一个新列表。

public static final ImmutableList<String> ANIMALS = ImmutableList.of(
        dog,
        cat,
        bird );

或者您可以制作现有列表的防御性副本。在这种情况下,您将获得一个新的单独列表。从原始列表中删除将不会影响(缩小)不可变列表。

ImmutableList<String> ANIMALS = ImmutableList.copyOf( originalList ); // defensive copy!

但请记住,虽然集合自己的定义是分开的,但包含的对象由原始列表和新的不可变列表共享。在制作防御性副本时,我们并没有复制 "dog" 对象。只有一个狗对象保留在内存中,两个列表都包含指向同一只狗的引用。如果 "dog" 对象中的属性被修改,两个集合都指向同一个狗对象,因此两个集合都将看到狗的新 属性 值。