Java不可变集合

128

根据Java 1.6集合框架文档

不支持任何修改操作(例如addremoveclear)的集合称为unmodifiable。...还保证了 Collection 对象中的任何更改都不会被看到的集合称为immutable

第二个标准让我有点困惑。假设第一个集合是unmodifiable的,并且假设原始集合引用已被丢弃,那么第二行所指的更改是什么?它是指集合中包含的元素的更改,也就是元素的状态吗?

第二个问题:
要使集合成为immutable,该如何提供指定的附加保证?如果集合中的元素状态由线程更新,那么将这些更改在持有immutable集合的线程上不可见是否足以实现不可变性?

要使集合成为immutable,该如何提供指定的附加保证?


通常(特别是在函数式编程语言中),不可变(又名持久化)集合可能会在某种意义上发生更改,这意味着您可以获取此集合的新状态,但同时旧状态仍可从其他链接中访问。例如,newCol = oldCol.add("element")将生成一个新集合,它是旧集合的副本,有1个以上的元素,并且所有对oldCol的引用仍将指向相同的未更改的旧集合。 - ffriend
7个回答

172
不可修改的集合通常是其他集合的只读视图(包装器)。您不能添加、删除或清除它们,但基础集合可以更改。
不可变集合根本无法更改——它们不包装另一个集合——它们有自己的元素。
这是来自guava的ImmutableList的一句话引用:
Collections.unmodifiableList(java.util.List<? extends T>)不同,后者是一个可以仍然更改的单独集合的视图,而ImmutableList的实例包含其自己的私有数据并且永远不会更改。
因此,基本上为了从可变集合中获取不可变集合,您必须将其元素复制到新集合中,并禁止所有操作。

3
如果包装在另一个不可修改的集合中的集合没有任何其他引用,那么不可修改的集合的行为是否与不可变集合完全相同? - Bhaskar
1
@Bozho,如果没有提供对支持集合的引用,只提供了不可修改的集合包装器引用,则您无法更改它。那么为什么要说“您不能确定”?这是否指出了一种情况,即某些线程或某种方式会修改它,因为支持集合是可修改的? - AKS
@AKS:当一个集合被包装在unmodifiableList中时,只接收对该列表的引用的代码将无法修改它,但是在创建包装器之前具有对原始列表的引用并且可以修改它的任何代码仍然可以这样做。如果创建原始列表的代码知道每个曾经存在于它身上的引用所发生的情况,并且知道它们中没有一个会落入可能修改列表的代码手中,那么它就可以知道该列表永远不会被修改。但是,如果引用是从外部代码接收的... - supercat
“unmodifiableList” 没有办法知道或者判断被包装的集合是否会发生变化,也无法知道变化的方式,使用它的代码同样如此。 - supercat
但是不可变集合只能在运行时阻止突变,对吗?...在编译时,你仍然可以尝试在其上进行突变操作?...没错吧? - rogue-one

94

不同之处在于,您不能拥有对允许更改的不可变集合的引用。不可修改的集合通过该引用是不可修改的,但是其他一些对象可能会指向相同的数据,通过这些对象可以进行更改。

例如:

List<String> strings = new ArrayList<String>();
List<String> unmodifiable = Collections.unmodifiableList(strings);
unmodifiable.add("New string"); // will fail at runtime
strings.add("Aha!"); // will succeed
System.out.println(unmodifiable);

1
如果原始集合引用未用于修改基础集合,那么有哪些原因可能会导致不可修改的集合发生变化? - Bhaskar

22
Collection<String> c1 = new ArrayList<String>();
c1.add("foo");
Collection<String> c2 = Collections.unmodifiableList(c1);

c1是可变的(即既不是不可修改的也不是不可变的)。
c2不可修改的:它本身不能被改变,但如果稍后我更改了c1,那么那个更改将在c2中可见。

这是因为c2只是c1的一个包装器而不是真正独立的副本。Guava提供ImmutableList接口和一些实现。它们通过实际创建输入的副本来工作(除非输入本身就是不可变集合)。

关于您的第二个问题:

集合的可变性/不可变性 不取决于 其中包含的对象的可变性/不可变性。修改集合中包含的对象不算作对集合的修改。当然,如果您需要一个不可变集合,则通常也希望它包含不可变对象。


2
@John:不,它不是。至少根据OP引用的定义来看不是。 - Joachim Sauer
抱歉,如果 c1 可以在其他地方被访问到,那么它就不是真正的不可变。然而,如果 c1 无法被访问到,则它将是不可变的。 - John Vint
1
@Vint:“如果c1没有逃逸”在规范中并没有任何区别的考虑。我认为,如果您可以使用集合使其不可变,则应始终将其视为非不可变的。 - Joachim Sauer
如果c2是全局变量,而c1在方法的本地上下文中定义,则将c2分配为Collection.unmodifiableList(c1);此时c1不可访问,则c2是不可变的。 - John Vint
1
让我引用定义:“集合还可以保证…”(强调我的)。Collection.unmodifiableList() 无法保证这一点,因为它无法保证其参数不会逃逸。Guava ImmutableList.of 始终生成一个不可变的List,即使您让其参数逃逸。 - Joachim Sauer
显示剩余3条评论

19

现在 Java 9 已经为不可变列表、集合、映射和映射项 (Map.Entry) 提供了工厂方法。

在 Java SE 8 和之前的版本中,我们可以使用 Collections 类实用程序方法(例如 unmodifiableXXX)来创建不可变集合对象。

然而,这些 Collections.unmodifiableXXX 方法是一种非常繁琐且冗长的方法。为了克服这些缺点,Oracle 公司已经向 List、Set 和 Map 接口添加了几个实用程序方法。

现在在 Java 9 中: List 和 Set 接口有“of()”方法,用于创建空或非空的不可变 List 或 Set 对象,如下所示:

空 List 示例

List immutableList = List.of();

非空列表示例

List immutableList = List.of("one","two","three");

4
在Java 10中,新增了List.copyOfSet.copyOf方法,可以创建一个不可修改的列表/集合的副本,如果给定的集合已经是不可修改的,则返回该集合本身。详见JDK-8191517 - Marcono1234

5
我认为重点在于,即使集合是Unmodifiable,也不能保证它不会发生变化。例如,一个集合如果元素太旧就会删除它们。Unmodifiable只是意味着持有引用的对象不能改变它,而不是它不能改变。一个真正的例子是Collections.unmodifiableList方法。它返回一个List的不可修改视图。传递到该方法中的List引用仍然是可修改的,因此持有该传递引用的任何人都可以修改该列表。这可能导致ConcurrentModificationExceptions和其他不良后果。
Immutable表示集合无法以任何方式进行更改。
第二个问题:Immutable集合并不意味着其中包含的对象不会发生变化,只是集合不会在它所持有的对象数量和组成方面发生变化。换句话说,集合的引用列表将不会改变。这并不意味着所引用对象的内部不能发生变化。

好的!如果我在Unmodifiable Collection上创建一个包装器,并将可修改集合引用设置为私有,那么我可以确保它是不可变的。对吗? - AKS
如果我理解你的问题,那么答案是否定的。将Unmodifiable进行包装对于保护它免受传递给unmodifiableList的集合修改没有任何作用。如果你想要这样做,请使用ImmutableList - John B

2

Pure4J以两种方式支持您所需的内容。

首先,它提供了一个@ImmutableValue注释,因此您可以对类进行注释以表示其是不可变的。有一个Maven插件可以让您检查您的代码是否实际上是不可变的(使用final等)。

其次,它提供了来自Clojure的持久化集合(具有泛型),并确保添加到集合中的元素是不可变的。这些集合的性能似乎非常好。集合都是不可变的,但实现了Java集合接口(和泛型)以进行检查。突变会返回新的集合。

免责声明:我是开发人员


0
在Java 9之前,使用Collections.unmodifiableXXX()方法创建不可修改的集合。这些方法只是像包装器方法一样的行为,返回原始集合的不可修改视图或只读视图。也就是说,您不能通过这些包装器方法返回的引用执行添加、删除、替换、清除等修改操作。但是,如果您有对它的其他引用,则可以修改原始集合,并且这些修改将反映在这些方法返回的视图中。
例如,
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
 
public class Java9ImmutableCollections 
{
    public static void main(String[] args) 
    {
        List<String> sportList = new ArrayList<String>();
         
        sportList.add("Hockey");
        sportList.add("Cricket");
        sportList.add("Tennis");
         
        List<String> unModifiableSportList = Collections.unmodifiableList(sportList);
 
        System.out.println(sportList);    //Output : [Hockey, Cricket, Tennis]
         
        System.out.println(unModifiableSportList);    //Output : [Hockey, Cricket, Tennis]
         
        unModifiableSportList.add("Wrestling");     //It gives run-time error
         
        sportList.add("Kabaddi");      //It gives no error and will be reflected in unModifiableSportList
         
        System.out.println(sportList);    //Output : [Hockey, Cricket, Tennis, Kabaddi]
         
        System.out.println(unModifiableSportList);    //Output : [Hockey, Cricket, Tennis, Kabaddi]
         
    }
}

从Java 9开始,引入了静态工厂方法来创建不可变集合。

1) Immutable List : List.of()
2) Immutable Set : Set.of()
3) Immutable Map : Map.of() or Map.ofEntries()

Immutable Vs Unmodifiable :

Java 9中的不可变集合和由Collections.unmodifiableXXX()包装器方法返回的不可修改集合并不相同。不可修改的集合只是原始集合的只读视图。您可以在原始集合上执行修改操作,并且这些方法返回的集合将反映这些修改。但是,Java 9静态工厂方法返回的不可变集合是100%不可变的。一旦创建,您无法修改它们。

来源:https://javaconceptoftheday.com/java-9-immutable-collections/


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