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

cod*_*tes 2 java collections immutability deep-copy shallow-copy

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

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

在下面的代码中,我无法理解这种情况:

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

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

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

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

提前致谢。

不可变学生.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();
    }
    
}

Run Code Online (Sandbox Code Playgroud)

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);

    }
}
Run Code Online (Sandbox Code Playgroud)

结果

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]
Run Code Online (Sandbox Code Playgroud)

小智 6

来自clone()Javadoc:

返回此 ArrayList 实例的浅表副本。(元素本身不会被复制。)

这意味着该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 ]
Run Code Online (Sandbox Code Playgroud)

对于您的问题:

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

这取决于您想要实现的目标。有两种情况:

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

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

这两种情况都可以通过使用 来回答Collections.unmodifiableList,它在 Javadoc 中说明:

返回指定列表的不可修改视图。对返回列表的查询操作“通读”到指定列表,并尝试修改返回的列表,无论是直接还是通过其迭代器,都会导致 UnsupportedOperationException。

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

根据您的描述,我相信您要实现的目标是scenario A.