java - 如何在 Java 中安全地使用不可变类中的集合?

标签 java collections immutability deep-copy shallow-copy

我尝试实现不可变类,我看到一条规则说明“在 getter 方法中执行对象克隆以返回副本而不是返回实际对象引用”。

我知道当我们使用不可变对象(immutable对象)时,从 getter 返回的复制/克隆集合不会发生任何变化。当我们使用自定义类时,原始集合的变化也可以从 getters 的克隆(浅拷贝)集合返回中看到。

在下面的代码中,我无法理解大小写:

我创建了两种方法,一种用于将原始集合作为类(class)列表返回,另一种用于类(class)列表的浅拷贝。

我为本地引用 clist1 和 clist2 分配了两个版本。

然后我更改了原始列表中的项目。当我通过学生对象到达它们时,我也可以看到更改原始列表和复制列表。但是,通过我之前指向克隆类(class)列表的引用无法看到更改!我认为它也应该受到变化的影响。 为什么我看不到以前复制的版本的变化?这是引用,我认为它应该指向相同的内存区域,我也通过下面的另一个例子再次检查结果。 我创建了一个包含 StringBuilder 的列表。我将新的 strs 应用到 stringbuilder,然后我可以看到之前复制的列表的更改版本。

那么,主要问题是,我必须始终在不可变类中使用深拷贝吗?这是错误的用法吗?在不可变类中使用集合的安全方法是什么?

提前致谢。

ImmutableStudent.java


public final class ImmutableStudent {

    public ImmutableStudent(String _name, Long _id, String _uni, ArrayList<String> _courseList){
        name = _name;
        id = _id;
        uni = _uni;
        courseList = new ArrayList<>();
        _courseList.forEach( course -> courseList.add(course));
    }

    private final String name;
    private final Long id;
    private final String uni;
    private final ArrayList<String> courseList;


    public String getName() {
        return name;
    }

    public Long getId() {
        return id;
    }

    public String getUni() {
        return uni;
    }


    public List<String> getCourseList() {
        return courseList;
    }

    public List<String> getCourseListClone() {
        return (ArrayList<String>)courseList.clone();
    }
    
}

ImmutableHelper.java

public class ImmutableHelper {

    public static void run(){

        ArrayList<String> courseList = new ArrayList<>();
        courseList.add("Literature");
        courseList.add("Math");

        String name = "Emma";
        Long id = 123456L;
        String uni = "MIT";

        ImmutableStudent student = new ImmutableStudent(name, id, uni, courseList);

        System.out.println(name == student.getName());
        System.out.println(id.equals(student.getId()));
        System.out.println(courseList == student.getCourseList());

        System.out.println("Course List         :" + student.getCourseList());
        System.out.println("Course List Clone   :" + student.getCourseListClone());

        List<String> clist1 = student.getCourseList();
        List<String> clist2 = student.getCourseListClone();

        student.getCourseList().set(1, "Art");

        System.out.println("Course List         :" + student.getCourseList());
        System.out.println("Course List Clone   :" + student.getCourseListClone());

        System.out.println("Previous Course List        :" + clist1);
        System.out.println("Previous Course List Clone  :" + clist2);
        

        // Check shallow copy using collections.clone()


        ArrayList<StringBuilder> bList = new ArrayList<>();

        StringBuilder a = new StringBuilder();
        a.append("1").append("2").append("3");

        StringBuilder b = new StringBuilder();
        b.append("5").append("6").append("7");

        bList.add(a);
        bList.add(b);

        ArrayList<StringBuilder> bListCp = (ArrayList<StringBuilder>)bList.clone();

        System.out.println("Blist   : " + bList);
        System.out.println("BlistCp :" + bListCp);

        a.append(4);

        System.out.println("Blist   : " + bList);
        System.out.println("BlistCp :" + bListCp);

    }
}

结果

Course List         :[Literature, Math]

Course List Clone   :[Literature, Math]

Course List         :[Literature, Math, Art]

Course List Clone   :[Literature, Math, Art]

Previous Course List        :[Literature, Math, Art]

Previous Course List Clone  :[Literature, Math]

Blist   : [123, 567]

BlistCp :[123, 567]

Blist   : [1234, 567]

BlistCp :[1234, 567]

最佳答案

来自 clone() Javadoc:

Returns a shallow copy of this ArrayList instance. (The elements themselves are not copied.)

这意味着 clone 方法返回的引用实际上是对包含与原始列表完全相同的元素的 ArrayList 新实例的引用。在一个例子中:

// Original list is reference L1 and contains three elements A, B and C
L1 = [ A, B, C ]

// By doing L1.clone you get a reference to a new list L2
L2 = [ A, B, C ]

// When you add a new element to L1 you do not see the change in L2 because
// it is effectively a different list
L1 = [ A, B, C, D ]
L2 = [ A, B, C ]

// However, if you change one of the A, B or C's internal state then that
// will be seen when accessing that object through L2, since the reference
// kept in the lists are the same
L1 = [ A, B', C, D ]
L2 = [ A, B', C ]

针对您的问题:

So, the main question, must I use the deep copy in immutable classes always ? Is this a wrong usage ? What would be the safe way to use collections in immutable classes ?

这取决于你想达到什么目的。有两种情况:

场景 A:您希望类的用户接收到列表的不可变 View (意味着没有用户可以修改列表),但您希望原始列表发生的任何更改是通过不可变 View 传播。

场景 B:您希望列表的所有版本都是不可变的,即使是内部保存的也是如此。

这两种情况都可以使用 Collections.unmodifiableList 来解决,它在 Javadoc 中指出:

Returns an unmodifiable view of the specified list. 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.

不同之处在于您使用它的地方。对于 Scenario A,您将调用 getter 中的方法,这样每个调用者都会收到列表的不可修改 View ,但该类仍会在内部保留可修改 View 。对于场景 B,您将在类内部存储对调用 unmodifiableList 的结果的引用。请注意,在较新版本的 Java 上,您还可以使用 List.copyOf 创建一个不可变列表,您可以将其用于场景 B

根据您的描述,我相信您要实现的是场景A

关于java - 如何在 Java 中安全地使用不可变类中的集合?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/66086923/

相关文章:

PostgreSQL 插入或更新触发函数波动类别

perl - 使对象实例不可变

Java JNI : Creating a Swing Window using JNI from C

java - 创建对象的基本java

java - 如何通过Java或SQL获取数据库表中定义的Json顺序?

java - Integer 和 int 数组的 containsAll 行为差异 (Java)

java - 是什么让 String 不可变?

java - Spring Rest CRUD 保存外键

java - 如何在android中正确声明静态变量?

java - Map.clear() 与新 map : Which one will be better?