如何在 Java 中对对象进行深度克隆

26 阅读5分钟

1. 简介

当我们想要在 Java 中复制一个对象时,我们需要考虑两种可能性:[浅复制和深复制]

对于浅复制方法,我们仅复制字段值,因此复制可能依赖于原始对象。在深复制方法中,我们确保树中的所有对象都经过深复制,因此复制不依赖于任何可能发生变化的先前存在的对象。

在本教程中,我们将比较这两种方法,并学习四种实现深度复制的方法。

2. Maven 设置

我们将使用三个 Maven 依赖项 Gson、Jackson 和 Apache Commons Lang 来测试执行深度复制的不同方法。

让我们将这些依赖项添加到我们的pom.xml中:

<dependency>
    <groupId>com.google.code.gson</groupId>
    <artifactId>gson</artifactId>
    <version>2.10.1</version>
</dependency>
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
    <version>3.14.0</version>
</dependency>
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.17.2</version>
</dependency>

可以在 Maven Central 上找到GsonJacksonApache Commons Lang的最新版本。

3. 模型

为了比较复制 Java 对象的不同方法,我们需要处理两个类:

class Address {

    private String street;
    private String city;
    private String country;

    // standard constructors, getters and setters
}
class User {

    private String firstName;
    private String lastName;
    private Address address;

    // standard constructors, getters and setters
}

4.浅拷贝

浅拷贝就是我们仅将字段的值从一个对象复制到另一个对象:

@Test
public void whenShallowCopying_thenObjectsShouldNotBeSame() {

    Address address = new Address("Downing St 10", "London", "England");
    User pm = new User("Prime", "Minister", address);
    
    User shallowCopy = new User(
      pm.getFirstName(), pm.getLastName(), pm.getAddress());

    assertThat(shallowCopy)
      .isNotSameAs(pm);
}

在这种情况下,pm != shallowCopy,这意味着它们是不同的对象;然而问题是,当我们改变任何原始地址的属性时,这也会影响shallowCopy的地址

如果Address是不可变的,我们就不会为此烦恼,但事实并非如此:

@Test
public void whenModifyingOriginalObject_ThenCopyShouldChange() {
 
    Address address = new Address("Downing St 10", "London", "England");
    User pm = new User("Prime", "Minister", address);
    User shallowCopy = new User(
      pm.getFirstName(), pm.getLastName(), pm.getAddress());

    address.setCountry("Great Britain");
    assertThat(shallowCopy.getAddress().getCountry())
      .isEqualTo(pm.getAddress().getCountry());
}

5.深层复制

深层复制是解决此问题的另一种方法。其优点是对象图中的每个可变对象都被递归复制

由于副本不依赖于任何之前创建的可变对象,因此它不会像我们在浅拷贝中看到的那样被意外修改。

在以下章节中,我们将讨论几种深度复制的实现并展示这一优势。

5.1. 复制构造函数

我们将研究的第一个实现是基于复制构造函数的:

public Address(Address that) {
    this(that.getStreet(), that.getCity(), that.getCountry());
}
public User(User that) {
    this(that.getFirstName(), that.getLastName(), new Address(that.getAddress()));
}

在上面的深度复制实现中,我们没有在复制构造函数中创建新的字符串,因为 String是一个不可变的类。

因此,它们不会被意外修改。让我们看看这是否有效:

@Test
public void whenModifyingOriginalObject_thenCopyShouldNotChange() {
    Address address = new Address("Downing St 10", "London", "England");
    User pm = new User("Prime", "Minister", address);
    User deepCopy = new User(pm);

    address.setCountry("Great Britain");
    assertNotEquals(
      pm.getAddress().getCountry(), 
      deepCopy.getAddress().getCountry());
}

5.2. Cloneable 接口

第二种实现是基于从Object继承的 clone 方法。它是受保护的,但我们需要将其覆盖为public

我们还将向类添加一个标记接口Cloneable, 以表明这些类实际上是可克隆的。

让我们将clone() 方法添加到Address类:

@Override
public Object clone() {
    try {
        return (Address) super.clone();
    } catch (CloneNotSupportedException e) {
        return new Address(this.street, this.getCity(), this.getCountry());
    }
}

现在让我们为User类实现 clone() :**

@Override
public Object clone() {
    User user = null;
    try {
        user = (User) super.clone();
    } catch (CloneNotSupportedException e) {
        user = new User(
          this.getFirstName(), this.getLastName(), this.getAddress());
    }
    user.address = (Address) this.address.clone();
    return user;
}

请注意,super.clone() 调用返回对象的浅表副本,但是我们手动设置了可变字段的深表副本,因此结果是正确的:

@Test
public void whenModifyingOriginalObject_thenCloneCopyShouldNotChange() {
    Address address = new Address("Downing St 10", "London", "England");
    User pm = new User("Prime", "Minister", address);
    User deepCopy = (User) pm.clone();

    address.setCountry("Great Britain");

    assertThat(deepCopy.getAddress().getCountry())
      .isNotEqualTo(pm.getAddress().getCountry());
}

6. 外部库

上面的例子看起来很简单,但有时当我们无法添加额外的构造函数或重写克隆方法时, 它们就不能作为解决方案。

当我们不拥有代码时,或者当对象图过于复杂时,如果我们专注于编写额外的构造函数或在对象图中的所有类上实现克隆方法,我们就无法按时完成项目,可能会发生这种情况。

那么我们能做什么呢?在这种情况下,我们可以使用外部库。为了实现深层复制,我们可以序列化一个对象,然后将其反序列化为一个新对象

我们来看几个例子。

6.1. Apache Commons 语言

Apache Commons Lang 具有SerializationUtils#clone,当对象图中的所有类都实现 Serializable接口时,它会执行深度复制。

如果该方法遇到不可序列化的类,它将会失败并抛出一个未经检查的SerializationException

因此,我们需要向我们的类添加Serializable接口:

@Test
public void whenModifyingOriginalObject_thenCommonsCloneShouldNotChange() {
    Address address = new Address("Downing St 10", "London", "England");
    User pm = new User("Prime", "Minister", address);
    User deepCopy = (User) SerializationUtils.clone(pm);

    address.setCountry("Great Britain");

    assertThat(deepCopy.getAddress().getCountry())
      .isNotEqualTo(pm.getAddress().getCountry());
}

6.2. 使用 Gson 进行 JSON 序列化

另一种序列化方式是使用 JSON 序列化。Gson 是一个用于将对象转换为 JSON 和将 JSON 转换为对象内容的库。

与 Apache Commons Lang 不同,GSON 不需要Serializable接口来进行转换

让我们快速看一个例子:

@Test
public void whenModifyingOriginalObject_thenGsonCloneShouldNotChange() {
    Address address = new Address("Downing St 10", "London", "England");
    User pm = new User("Prime", "Minister", address);
    Gson gson = new Gson();
    User deepCopy = gson.fromJson(gson.toJson(pm), User.class);

    address.setCountry("Great Britain");

    assertThat(deepCopy.getAddress().getCountry())
      .isNotEqualTo(pm.getAddress().getCountry());
}

6.3. 使用 Jackson 进行 JSON 序列化

Jackson 是另一个支持 JSON 序列化的库。此实现与使用 Gson 的实现非常相似,但我们需要向类中添加默认构造函数

我们来看一个例子:

@Test
public void whenModifyingOriginalObject_thenJacksonCopyShouldNotChange() 
  throws IOException {
    Address address = new Address("Downing St 10", "London", "England");
    User pm = new User("Prime", "Minister", address);
    ObjectMapper objectMapper = new ObjectMapper();
    
    User deepCopy = objectMapper
      .readValue(objectMapper.writeValueAsString(pm), User.class);

    address.setCountry("Great Britain");

    assertThat(deepCopy.getAddress().getCountry())
      .isNotEqualTo(pm.getAddress().getCountry());
}

7. 结论

进行深层复制时,我们应该使用哪种实现?最终的决定通常取决于我们要复制的类,以及我们是否拥有对象图中的类。