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.16.1</version>
</dependency>

最新版本可在Maven Central获取:

3. 模型类

为测试不同拷贝方法,我们定义两个类:

class Address {

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

    // 标准构造器、getter和setter
}
class User {

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

    // 标准构造器、getter和setter
}

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(不同对象),但修改原始address会影响shallowCopy

@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());
}

⚠️ 踩坑点:如果Address不可变则无此问题,但它是可变的。

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接口

重写clone()方法并实现Cloneable接口:

@Override
public Object clone() {
    try {
        return (Address) super.clone();
    } catch (CloneNotSupportedException e) {
        return new Address(this.street, this.getCity(), this.getCountry());
    }
}
@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 Lang

使用SerializationUtils#clone,要求对象图所有类实现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());
}

限制:遇到非Serializable类会抛出SerializationException

6.2. Gson JSON序列化

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要求类有无参构造器:

@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. 总结

选择深拷贝实现方式时需考虑:

  1. 是否拥有类控制权

    • ✅ 可修改类:拷贝构造器或Cloneable
    • ❌ 不可修改类:外部库方案
  2. 对象图复杂度

    • 简单对象:手动实现
    • 复杂对象:JSON序列化更高效
  3. 性能要求

    • 高频场景:拷贝构造器性能最佳
    • 低频场景:JSON序列化开发效率更高

完整代码示例见GitHub仓库


原始标题:How to Make a Deep Copy of an Object in Java | Baeldung