Collections.unmodifiableList 和防御性复制

50
如果我写
List<Integer> a1 = Arrays.asList(1, 2, 3);
List<Integer> a2 = Collections.unmodifiableList(a1);

a2是只读的,但如果我写入

a1.set(0,10);

如果修改原始集合,那么目标复制集合也会被修改。

如果API中说:

返回指定集合的不可修改视图。此方法允许模块向用户提供对内部集合的“只读”访问权限。

那么,为什么我修改原始集合后,目标复制集合也被修改了呢?

也许我误解了意思,如果是这样,该如何编写该集合的防御性副本呢?


1
如果您想要一个不可修改的a1版本,那么您可以克隆a1并将其变为不可修改的列表。然后对a1的更新将不会影响a2。 - Steve W
9个回答

55
是的,你理解得很正确。这个想法是,umodifiableCollection返回的对象不能直接改变,但可以通过其他方式进行更改(实际上是通过直接更改内部集合进行更改)。

只要有东西能够访问内部列表,“不可修改”的集合就可以被更改。
这就是为什么你通常创建一个不可修改的集合,并确保任何东西都无法访问内部列表的原因:
Collection<Integer> myUmodifiableCollection = Collection.umodifiableCollection(Arrays.asList(1, 2, 3));

由于没有任何东西引用由 asList 创建的 List,所以这是一个真正的不可修改的集合。
这种方法的优点是,您根本不需要复制原始集合/列表,这避免了使用内存和计算能力。 Guava 提供 ImmutableCollection 类(以及其子类,比如 ImmutableList),它们提供真正的不可变集合(通常通过复制源来实现)。

Arrays.asList() 已经返回了一个固定大小的列表。那么为什么我们还需要使用 Collections.unmodifiableList() 进行装饰呢?我看到你和 @assylias 都使用了相同的习语。你能解释一下吗? - Geek
8
@Geek:虽然Arrays.asList()返回的是一个固定大小的列表,但它并不是不可修改的。你仍然可以使用set(int, T)方法更改元素。Collections.unmodifiableList()通过禁止修改来“修复”这个问题。但在任何实际的代码中,我可能会直接使用Guava的ImmutableList(上面提供了链接),因为它书写更短,非常清晰,并且始终确保不会出现“泄漏”。 - Joachim Sauer

12

也许我误解了其含义,如果是这样,那么编写该集合的防御性副本的方法是什么?

通常,您会这样使用它:

private List<Integer> a1 = Arrays.asList(1, 2, 3);

public List<Integer> getUnmodifiable() {
    return Collections.unmodifiableList(a1);
}

调用getUnmodifiable方法的人,如果没有访问您类的内部(即他们无法访问私有变量a1),则无法修改返回的列表。


1
Arrays.asList() 已经返回了一个固定大小的列表。那么为什么我们还需要使用 Collections.unmodifiableList() 来装饰它呢?我错过了什么吗? - Geek
4
您无法向a1添加内容,但可以将任何项设置为不同的内容。 - assylias
1
@assylias 实际上,您可以修改列表中对象的内部,但是您不能使用set()方法将旧对象替换为另一个对象。 - Sam003

4
如果您想保持a1的可变性,并在没有Guava的情况下创建一个不可变副本,这是一种方法。
    List<Integer> a1 = Arrays.asList(1, 2, 3);
    List<Integer> a2 = Collections.unmodifiableList(new ArrayList<>(a1));

2
自Java 10开始,您可以使用List.copyOf()函数:
List<Integer> a1 = Arrays.asList(1, 2, 3);
List<Integer> a2 = List.copyOf(a1);

在那之后,对 a1 的更改不能通过 a2 看到,因为 List.copyOf() 将您的列表内容复制到一个新列表中。副本是不可修改的。但在使用该方法之前,请务必阅读该方法的文档,因为它不适用于包含 null 值的列表。


1
这个想法是你不能通过 a2 修改列表。
修改 a1 列表确实会修改你在 a2 中看到的内容 - 这是有意的。
只需不公开访问列表 a1,你就可以得到想要的结果 :)

1
API表示它在您的情况下使用内部集合,但是集合不是内部的。
重点是,当类中有一个私有列表和该列表的getter时,您可能希望getter的调用者无法修改列表,这种情况下,您将不得不返回一个不可修改的列表。否则,返回的列表只是对您的内部/私有列表的引用,因此其内容可以被修改。

1

该语句:

Collections.unmodifiableList(a1);

返回原始集合的包装器,其修改器方法抛出UnsupportedOperationException

包装器是读取的,这意味着如果您修改a1,更改将反映在包装的集合a2上。


1
实际上,您可以修改列表中对象的内部,但是您不能使用set()方法将旧对象替换为另一个对象。 - Sam003

0
如果你需要一个不可修改和不可变的列表,或者换句话说,没有与其他库有任何依赖关系的源列表的不可修改副本,请尝试以下方法:
Collections.unmodifiableList(Collections.list(Collections.enumeration(sourceList)))

Collections.list() 方法从枚举中复制值。


0

a1a2 将引用相同的数据(内存)。

仅有 a2 作为入口,才能让部分内容无法修改。

如果你将 a2 传递给一个期望方法是幂等的方法中,那么 a2 是有用的。

总之,你不能使用 a2 指针来修改数据。


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