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

How do I use collections in immutable class safely in Java?

我尝试实现不可变 class,我看到一条规则说明“将 getter 方法中的对象克隆到 return 副本而不是 returning 实际对象引用"。

我知道当我们使用不可变对象时,从 getter 复制/克隆的集合 return 不会有任何变化。当我们使用自定义 classes 时,原始集合中的变化也可以从 getters.

克隆(浅拷贝)集合 return 中看到

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

我创建了两种方法,一种用于 return 作为 courseList 的原始集合,另一种用于课程列表的浅表副本。

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

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

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

提前致谢。

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() Java文档:

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: 您希望 class 的用户接收列表的不可变视图(意味着没有用户可以修改列表)但您想要任何原始列表发生的更改将通过不可变视图传播。

场景 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 中的方法,以便每个调用者都将收到列表的不可修改视图,但 class 仍将在内部保留可修改视图。对于 Scenario B,您将在 class 内部存储对调用 unmodifiableList 结果的引用。请注意,在较新版本的 Java 上,您还可以使用 List.copyOf 创建一个不可变列表,您可以将其用于 Scenario B.

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

Why I cant see the change on previously copied version ?

正因为你抄袭了!这是一个副本 - 与原始对象不同的 ArrayList 对象,恰好包含相同的元素。

This is reference and I think it should be point the same memory area

只有在以下情况下才是正确的:

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

这就是您在 clist1 上看到变化的原因。使用 clone(),您将创建一个新对象并分配新内存。当然,您仍在 return 对对象进行 引用 ,但它与 courseList 存储的引用不同。这是对副本的引用。

must I use the deep copy in immutable classes always ?

不,只要集合中的元素是不可变的。制作副本的全部目的是让用户不能做这样的事情:

List<String> list = student.getCourseList();
list.add("New Course");

如果 getCourseList 没有 return 副本,以上代码将更改 student 的课程列表!我们当然不希望这种情况发生在不可变的 class 中,对吗?

如果列表元素也是不可变的,那么您的 class 用户将无法更改它们,因此您无需复制列表元素。


当然,如果您只使用 不可变列表:

,则可以避免所有这些复制
private final List<String> courseList;
public ImmutableStudent(String _name, Long _id, String _uni, ArrayList<String> _courseList){
    name = _name;
    id = _id;
    uni = _uni;
    courseList = Collections.unmodifiableList(_courseList)
};