1. 引言
在这个简短的教程中,我们将学习如何在 Java 中复制 ArrayList
,重点是创建列表元素的深度拷贝的不同方法。
2. 浅拷贝与深拷贝
浅拷贝技术复制原始对象,但只复制可变字段的引用,而不是实际的对象。另一方面,深拷贝会创建所有可变字段(包括嵌套对象)的独立副本。有关详细指南,请参考我们的文章:深拷贝与浅拷贝的区别。
3. 模型
让我们创建两个类:Course
和 Student
。Student
类有一个可变依赖的 Course
对象实例:
public class Course {
private Integer courseId;
private String courseName;
// standard getters and setters
}
public class Student {
private int studentId;
private String studentName;
private Course course;
// standard getters and setters
}
4. 通过 Cloneable
接口实现深拷贝
让我们在模型类中实现 Cloneable
标记接口并重写 clone
方法来创建深度拷贝:
@Override
public Course clone() {
try {
return (Course) super.clone();
} catch (CloneNotSupportedException e) {
throw new IllegalStateException(e);
}
}
注意,super.clone()
总是返回对象的浅拷贝。在 Course
类中,我们没有可变字段,而在 Student
类中,我们需要显式设置可变字段以创建深度拷贝:
@Override
public Student clone() {
Student student;
try {
student = (Student) super.clone();
} catch (CloneNotSupportedException e) {
throw new IllegalStateException(e);
}
student.course = this.course.clone();
return student;
}
现在,让我们遍历项目并使用 clone
方法,验证已经创建了深度拷贝:
public static List<Student> deepCopyUsingCloneable(List<Student> students){
return students.stream().map(Student::clone).collect(Collectors.toList());
}
@Test
public void whenCreatingCopyWithCloneable_thenObjectsShouldNotBeSame() {
Course course = new Course(1, "Spring Masterclass");
Student student1 = new Student(1, "John", course);
Student student2 = new Student(2, "David", course);
List<Student> students = new ArrayList<>();
students.add(student1);
students.add(student2);
List<Student> deepCopy = Student.deepCopyUsingCloneable(students);
Assertions.assertEquals(students.get(0), deepCopy.get(0));
Assertions.assertNotSame(students.get(0),deepCopy.get(0));
Assertions.assertEquals(students.get(1), deepCopy.get(1));
Assertions.assertNotSame(students.get(1),deepCopy.get(1));
}
5. 使用复制构造函数进行深拷贝
复制构造函数是一个特殊的构造函数,它接受其类类型的参数,并返回一个具有传递值的新类实例。
让我们为 Student
对象创建一个复制构造函数,然后用它来对列表中的每个项目进行深度拷贝:
public Student(Student student) {
this.studentId = student.getStudentId();
this.studentName = student.getStudentName();
this.course = new Course(student.getCourse()
.getCourseId(), student.getCourse()
.getCourseName());
}
接下来,让我们遍历列表中的项目,并使用上面创建的复制构造函数对列表中的每个项目进行深度拷贝,然后返回一个新的列表:
public static List<Student> deepCopyUsingCopyConstructor(List<Student> students){
return students.stream().map(Student::new).collect(Collectors.toList());
}
在这种情况下,修改原始 ArrayList
或列表中的元素不会影响复制的列表,反之亦然:
@Test
public void whenCreatingDeepCopyWithCopyConstructor_thenObjectsShouldNotBeSame() {
Course course = new Course(1, "Spring Masterclass");
Student student1 = new Student(1, "John", course);
Student student2 = new Student(2, "David", course);
List<Student> students = new ArrayList<>();
students.add(student1);
students.add(student2);
List<Student> deepCopy = Student.deepCopyUsingCopyConstructor(students);
Assertions.assertEquals(students.get(0), deepCopy.get(0));
Assertions.assertNotSame(students.get(0),deepCopy.get(0));
Assertions.assertEquals(students.get(1), deepCopy.get(1));
Assertions.assertNotSame(students.get(1),deepCopy.get(1));
}
6. 使用 Apache Commons 库进行深拷贝
Apache Commons 库提供了一个名为 SerializationUtils.clone()
的实用方法,它使用序列化和反序列化帮助创建对象的深度拷贝。有关序列化的详细指南,请参阅我们的文章:Java 序列化。
这种方法确保所有字段,包括嵌套对象,都被复制,从而得到一个完全独立的深度拷贝:
public static List<Student> deepCopyUsingSerialization(List<Student> students){
return students.stream().map(SerializationUtils::clone).collect(Collectors.toList());
}
为了成功,对象图中的所有对象都必须实现 Serializable
接口。否则,将抛出异常:
@Test
public void whenCreatingDeepCopyWithSerializationUtils_thenObjectsShouldNotBeSame() {
Course course = new Course(1, "Spring Masterclass");
Student student1 = new Student(1, "John", course);
Student student2 = new Student(2, "David", course);
List<Student> students = new ArrayList<>();
students.add(student1);
students.add(student2);
List<Student> deepCopy = Student.deepCopyUsingSerialization(students);
Assertions.assertEquals(students.get(0), deepCopy.get(0));
Assertions.assertNotSame(students.get(0),deepCopy.get(0));
Assertions.assertEquals(students.get(1), deepCopy.get(1));
Assertions.assertNotSame(students.get(1),deepCopy.get(1));
}
这为我们省去了处理复杂对象结构的克隆逻辑。然而,由于序列化和反序列化的开销,这种方法比其他方法稍慢一些。
您可以在 Maven 中央仓库找到最新版本的 apache-commons-lang3 库。
7. 使用 Jackson 库进行深拷贝
Jackson 是另一个使用序列化和反序列化创建原始对象深度拷贝的库。它将对象序列化为 JSON 字符串,然后再反序列化回一个新的独立副本:
public static Student createDeepCopy(Student student) {
ObjectMapper objectMapper = new ObjectMapper();
try {
return objectMapper.readValue(objectMapper.writeValueAsString(student), Student.class);
} catch (JsonProcessingException e) {
throw new IllegalArgumentException(e);
}
}
public static List<Student> deepCopyUsingJackson(List<Student> students) {
return students.stream().map(Student::createDeepCopy).collect(Collectors.toList());
}
请注意,Jackson 需要存在默认构造函数才能序列化和反序列化任何给定对象:
@Test
public void whenCreatingDeepCopyWithJackson_thenObjectsShouldNotBeSame() {
Course course = new Course(1, "Spring Masterclass");
Student student1 = new Student(1, "John", course);
Student student2 = new Student(2, "David", course);
List<Student> students = new ArrayList<>();
students.add(student1);
students.add(student2);
List<Student> deepCopy = Student.deepCopyUsingJackson(students);
Assertions.assertEquals(students.get(0), deepCopy.get(0));
Assertions.assertNotSame(students.get(0),deepCopy.get(0));
Assertions.assertEquals(students.get(1), deepCopy.get(1));
Assertions.assertNotSame(students.get(1),deepCopy.get(1));
}
您可以在 Maven 中央仓库找到最新版本的 jackson-databind 库。
8. 总结
在这篇教程中,我们探讨了复制 ArrayList
的各种方式,包括原生方法和第三方库的使用。如往常一样,源代码可在 GitHub 上获取。