1. 引言

在这个简短的教程中,我们将学习如何在 Java 中复制 ArrayList,重点是创建列表元素的深度拷贝的不同方法。

2. 浅拷贝与深拷贝

浅拷贝技术复制原始对象,但只复制可变字段的引用,而不是实际的对象。另一方面,深拷贝会创建所有可变字段(包括嵌套对象)的独立副本。有关详细指南,请参考我们的文章:深拷贝与浅拷贝的区别

3. 模型

让我们创建两个类:CourseStudentStudent 类有一个可变依赖的 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 上获取。