Java对象的深拷贝

综述

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

对于浅拷贝方法,我们只拷贝字段值,因此拷贝可能依赖于原始对象。在深度复制方法中,我们确保树中的所有对象都被深度复制,因此副本不依赖于任何可能会更改的先前存在的对象。

Maven设置

我们将使用三个Maven依赖项Gson、Jackson和Apache Commons Lang来测试深拷贝的不同方式。

<dependency>
    <groupId>com.google.code.gson</groupId>
    <artifactId>gson</artifactId>
    <version>2.8.2</version>
</dependency>
<dependency>
    <groupId>commons-lang</groupId>
    <artifactId>commons-lang</artifactId>
    <version>2.6</version>
</dependency>
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.9.3</version>
</dependency>

Model

为了比较复制 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
}

浅拷贝

浅拷贝是一种只将字段的值从一个对象复制到另一个对象。A shallow copy is one in which we only copy values of fields from one object to another:

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

深拷贝

深拷贝是解决此问题的替代方案。它的优点是对象图中的每个可变对象都是递归复制的(each mutable object in the object graph is recursively copied.)。

因为复制不依赖于之前创建的任何可变对象,所以它不会像我们在浅复制中看到的那样被意外修改。

复制构造函数(Copy Constructor)

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,因为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());
}

Cloneable接口

该实现基于从Object继承的clone方法。它是受保护(protected)的,但我们需要对它进行重写为public的。

我们还需要向类添加一个标记接口 Cloneable,以表明这些类实际上是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());
}

External Libraries

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

比如当我们没有源代码时,或者当object graph非常复杂,如果我们专注于编写额外的构造函数或在对象图中的所有类上实现clone()方法。

为了实现深度复制,我们可以序列化一个对象,然后将其反序列化为一个新对象。

Apache Commons Lang

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

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

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());
}
上一篇:60个提问模型,让你比别人更快成为高级PM


下一篇:postman使用教程8-设置断言(Tests脚本编写)