如何解决如何在 Java 中安全地使用不可变类中的集合?
我尝试实现不可变类,我看到一条规则说明“在 getter 方法中执行对象克隆以返回副本而不是返回实际对象引用”。
我知道当我们使用不可变对象时,从 getter 返回的复制/克隆集合不会发生变化。当我们使用自定义类时,也可以看到原始集合的变化,也可以看到从 getter 返回的克隆(浅复制)集合。
在下面的代码中,我无法理解这种情况:
我创建了两种方法,一种用于将原始集合作为 courseList 返回,另一种用于 courselist 的浅拷贝。
我为本地引用 clist1 和 clist2 分配了两个版本。
然后我更改了原始列表中的项目。当我通过学生对象到达它们时,我也可以看到更改的原始列表和复制的列表。但是,通过我之前指向克隆课程列表的参考,无法看到更改!我认为它也应该受到变化的影响。 为什么我看不到以前复制的版本的变化? 这是参考,我认为它应该指向相同的内存区域,我还通过下面的另一个示例再次检查结果。 我创建了一个包含 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,Art]
PrevIoUs Course List :[Literature,Art]
PrevIoUs Course List Clone :[Literature,Math]
Blist : [123,567]
BlistCp :[123,567]
Blist : [1234,567]
BlistCp :[1234,567]
解决方法
来自 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,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,C,D ]
L2 = [ A,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 ]
对于您的问题:
那么,主要问题是,我必须始终在不可变类中使用深拷贝吗?这是错误的用法吗?在不可变类中使用集合的安全方法是什么?
这取决于您想要实现的目标。有两种情况:
场景 A:您希望类的用户收到列表的不可变视图(意味着没有用户可以修改列表),但您希望原始列表发生的任何更改通过不可变视图传播。
场景 B:您希望列表的所有版本都是不可变的,即使是内部保存的版本也是如此。
这两种情况都可以通过使用 Collections.unmodifiableList
来回答,它在 Javadoc 中声明:
返回指定列表的不可修改视图。对返回列表的查询操作“通读”到指定列表,并尝试修改返回的列表,无论是直接还是通过其迭代器,都会导致 UnsupportedOperationException。
不同之处在于你在哪里使用它。对于 Scenario A
,您将调用 getter 中的方法,以便每个调用者都会收到列表的不可修改视图,但该类仍会在内部保留可修改视图。对于 Scenario B
,您将在类内部存储对调用 unmodifiableList
的结果的引用。请注意,在较新版本的 Java 上,您还可以使用 List.copyOf
创建不可变列表,您可以将其用于 Scenario B
。
根据您的描述,我相信您想要达到的目标是 scenario A
。
为什么我看不到以前复制的版本的变化?
正是因为你复制了它!它是一个副本 - 与原始对象不同的 ArrayList
对象,恰好包含相同的元素。
这是引用,我认为应该指向相同的内存区域
这仅适用于以下情况:
public List<String> getCourseList() {
return courseList;
}
这就是您在 clist1
上看到变化的原因。使用 clone()
,您正在创建一个新对象并分配新内存。当然,您仍然返回一个引用到一个对象,但它与 courseList
存储的引用不同。这是对副本的引用。
我必须始终在不可变类中使用深拷贝吗?
不需要,只要集合中的元素是不可变的。制作副本的全部目的是让用户不能做这样的事情:
List<String> list = student.getCourseList();
list.add("New Course");
如果getCourseList
没有返回副本,上面的代码会改变student
的课程列表!我们当然不希望在不可变的类中发生这种情况,对吗?
如果列表元素也是不可变的,那么你的类的用户无论如何都不能改变它们,所以你不需要复制列表元素。
当然,如果您只使用不可变列表,则可以避免所有这些复制:
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)
};
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。