如何在Java中安全地使用不可变类中的集合?
我试图实现immutable类,并且我看到一条规则声明“在getter方法中执行对象克隆以返回副本,而不是返回实际的对象引用” 我理解,当我们使用不可变时,从getter返回的复制/克隆的集合不会有任何更改。当我们使用自定义类时,可以看到原始集合中的更改,也可以看到从getter返回的克隆(浅拷贝)集合 在下面的代码中,我无法理解该案例: 我创建了两个方法,一个用于将原始集合作为courseList返回,另一个用于courseList的浅拷贝 我为本地引用clist1和clist2分配了两个版本 然后我更改了原始列表中的项目。当我通过学生对象访问原始列表和复制列表时,我也可以看到它们。但是,通过我之前指向克隆课程列表的参考,无法看到更改!我认为它也应该受到变化的影响为什么我看不到以前复制版本的更改?这是参考,我认为应该指向相同的内存区域,我还通过下面的另一个示例再次检查结果。 我创建了一个包含StringBuilder的列表。我向stringbuilder显示了新的STR,然后我可以看到以前复制的列表的更改版本 那么,主要问题是,我必须始终在不可变类中使用深度副本吗?这是一个错误的用法吗?在不可变类中使用集合的安全方法是什么? 提前谢谢 ImmutableStudent.java如何在Java中安全地使用不可变类中的集合?,java,collections,immutability,deep-copy,shallow-copy,Java,Collections,Immutability,Deep Copy,Shallow Copy,我试图实现immutable类,并且我看到一条规则声明“在getter方法中执行对象克隆以返回副本,而不是返回实际的对象引用” 我理解,当我们使用不可变时,从getter返回的复制/克隆的集合不会有任何更改。当我们使用自定义类时,可以看到原始集合中的更改,也可以看到从getter返回的克隆(浅拷贝)集合 在下面的代码中,我无法理解该案例: 我创建了两个方法,一个用于将原始集合作为courseList返回,另一个用于courseList的浅拷贝 我为本地引用clist1和clist2分配了两个版本
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();
}
}
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);
}
}
从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 ]
关于你的问题:
所以,主要的问题是,我必须始终在不可变类中使用深度副本吗?这是一个错误的用法吗?在不可变类中使用集合的安全方法是什么
这取决于你想要实现什么。有两种情况:
场景A:您希望类的用户接收到列表的不可变视图(这意味着任何用户都不能修改列表),但希望对原始列表所做的任何更改都通过不可变视图传播
场景B:您希望列表的所有版本都是不可变的,即使是内部保留的版本
这两种情况都可以通过使用集合来回答。不可修改列表,它在Javadoc中声明:
返回指定列表的不可修改视图。对返回列表的查询操作“通读”到指定的列表,并尝试修改返回的列表,无论是直接修改还是通过其迭代器修改,都会导致UnsupportedOperationException
区别在于你在哪里使用它。对于场景A
,您将在getter中调用该方法,这样每个调用方都将收到列表的不可修改视图,但类仍将在内部保留该可修改视图。对于场景B
,您将在类内部存储对调用不可修改列表的结果的引用。请注意,在较新版本的Java上,您还可以使用List.copyOf
创建一个不可变列表,您可以将其用于场景B
根据您的描述,我相信您试图实现的是场景A
为什么我在以前复制的版本上看不到更改
正是因为你复制了它!它是一个副本-一个与原始对象不同的ArrayList
对象,恰好包含相同的元素
这是参考,我认为它应该指向相同的内存区域
只有在以下情况下才是如此:
public List<String> getCourseList() {
return courseList;
}
如果getCourseList
没有返回副本,上述代码将更改学生的课程列表!我们当然不希望这种情况发生在不可变的类中,是吗
如果列表元素也是不可变的,那么类的用户无论如何都不能对它们进行变异,因此不需要复制列表元素
当然,如果只使用不可变列表,则可以避免所有这些复制:
私人最终名单课程列表;
public ImmutableStudent(String\u name、Long\u id、String\u uni、ArrayList\u courseList){
名称=_名称;
id=_id;
uni=_uni;
courseList=集合。不可修改列表(\u courseList)
};
rpsrosario,我非常感谢您的回答。这篇文章非常详细,有助于我更好地理解这个主题。谢谢你的回答。您说我在更改之前复制了列表,因此在复制的版本上看不到更改。但是,我也将bList复制为bListCp,我可以在这两个版本中看到内部状态的变化。复制cList时,会为列表中复制的项目分配新的内存区域,并为cList2创建新的引用列表。因此,我以后看不到这两个列表相同。当将bList复制为包含StringBuilder对象的bListCp时,在后台执行什么操作?在这一点上我有点困惑。@coderates您对bList所做的“更改”实际上并不是对bList的更改。相反,它是对bList中的一个StringBuilder'对象的更改。你没有抄这些,对吗?另一方面,您不可能更改cList中的
String`对象,因为字符串是不可变的。是的,我想我现在明白了。“如果列表元素也是不可变的,那么你的类的用户无论如何都不能对它们进行变异,所以你不需要复制列表元素。”你说,这将是最后一次检查1。因此,如果我是你
public List<String> getCourseList() {
return courseList;
}
List<String> list = student.getCourseList();
list.add("New Course");
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)
};