Java深拷贝的几种方法

1. 简介

在Java中拷贝对象有深拷贝和浅拷贝两种。本文将对这两种方法进行比较,并学习四种实现深拷贝的方法。

2. Maven依赖

后文实现深拷贝会依赖两个工具 Gson、Jackson 及 apache commons lang。为了进行单元测试我们这里使用assertj流式断言库。

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

<dependency>
    <groupId>org.assertj</groupId>
    <artifactId>assertj-core</artifactId>
    <version>3.11.1</version>
</dependency>

3.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
}

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产生影响。

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


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

这种实现通过手工的方式将类的各个成员进行赋值。String是不可变类,因此不需要创建对象的构造器来赋值。
创建完毕后,修改一个对象不会影响另一个对象中的值。

5.2 Cloneable接口

第二种实现基于Object对象的clone方法。该方法由protected修饰,我们需要覆盖并调整为public。接着我们在Address类中增加clone()方法。在clone方法内创建深拷贝对象。

@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()返回一个浅拷贝对象,但是我们通过调用address的clone方法返回一个深拷贝对象最终对User实现深拷贝。

@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

Apache Commons Lang有一个SerializationUtils#clone方法,该方法会执行深拷贝。但所拷贝的对象及其依赖的对象必须实现Serializable接口。如果尝试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());
}

6.2 通过Gson进行JSON序列化JSON Serialization With Gson

Gson可以用来将对象转换为JSON,反之亦然。与Apache Commons Lang不同,GSON在转换时不需要对象实现Serializable接口。因此我们可以将对象先序列化为JSON字符串,随后再将JSON字符串反序列化为对象。这样实现对象的深拷贝。

@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的序列化。但使用jackson时,需要给我们的类增加默认的构造器。其处理思路和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.总结

本文介绍了深拷贝的方法及如何通过第三方库去进行拷贝。实际应用可根据情况选择使用。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值