干货文:如何在 Java 中制作对象的深层副本

简介: 干货文:如何在 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.13.0</version>
</dependency>

你也可以指定最新的版本,最新版本的 Gson,Jackson 和 Apache Commons Lang 可以在 Maven 仓库上找到。https://mvnrepository.com/

三、弄两个类

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

public class Address {
    private String street;
    private String city;
    private String country;
    // getters and setters 方法
    // toString 方法
}
public class User {
    private String uname;
    private String job;
    private Address address;
    // getters and setters 方法
    // toString 方法
}

四、浅拷贝

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

// 当浅拷贝时,对象不是同一个
@Test
public void test() {
    Address address = new Address("天府路", "广州", "中国");
    User pm = new User("cuihua", "manager", address);
    
    User shallowCopy = new User(
      pm.getUname(), pm.getJob(), pm.getAddress());
    assertThat(shallowCopy)
      .isNotSameAs(pm);
}

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

如果地址是不可变的,我们不会打扰它,但它不是:

// 当我们更改任何原始地址的属性时,浅拷贝会被影响
@Test
public test2() {
 
    Address address = new Address("天府路", "广州", "中国");
    User pm = new User("cuihua", "manager", address);
    User shallowCopy = new User(
      pm.getUname(), pm.getJob(), pm.getAddress());
    address.setCountry("US");
    assertThat(shallowCopy.getAddress().getCountry())
      .isEqualTo(pm.getAddress().getCountry());
}

五、深拷贝

深拷贝是解决此问题的替代方法。它的优点是对象图中的每个可变对象都是递归复制的。

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

在以下部分中,我们将讨论几种深层复制实现并演示此优势。

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 test3() {
    Address address = new Address("天府路", "广州", "中国");
    User pm = new User("cuihua", "manager", address);
    User deepCopy = new User(pm);
    address.setCountry("US");
    
    assertNotEquals(
      pm.getAddress().getCountry(), 
      deepCopy.getAddress().getCountry());
}

5.2 克隆接口

第二个实现基于从 Object 继承的克隆方法。它受到保护,但我们需要将其覆盖为公共。

我们还将向类添加一个标记接口 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.getUname(), this.getJob(), this.getAddress());
    }
    user.address = (Address) this.address.clone();
    return user;
}

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

@Test
public void test4() {
    Address address = new Address("天府路", "广州", "中国");
    User pm = new User("cuihua", "manager", address);
    User deepCopy = (User) pm.clone();
    address.setCountry("US");
    assertThat(deepCopy.getAddress().getCountry())
      .isNotEqualTo(pm.getAddress().getCountry());
}

六、外部类

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

当我们不拥有代码时,或者当对象图非常复杂以至于如果我们专注于编写其他构造函数或在对象图中的所有类上实现克隆方法时,我们可能会按时完成项目。

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

让我们看几个例子。

6.1 Apache Commons Lang

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

如果该方法遇到不可序列化的类,它将失败并引发未经检查的序列化异常。

因此,我们需要将可序列化接口添加到我们的类中:

@Test
public void test5() {
    Address address = new Address("天府路", "广州", "中国");
    User pm = new User("cuihua", "manager", address);
    User deepCopy = (User) SerializationUtils.clone(pm);
    address.setCountry("US");
    assertThat(deepCopy.getAddress().getCountry())
      .isNotEqualTo(pm.getAddress().getCountry());
}

6.2. 使用 gson 进行 JSON 序列化

序列化的另一种方法是使用 JSON 序列化。Gson 是一个用于将对象转换为 JSON 的库,反之亦然。

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

让我们快速看一个例子:

@Test
public void test6() {
    Address address = new Address("天府路", "广州", "中国");
    User pm = new User("cuihua", "manager", address);
    
    Gson gson = new Gson();
    User deepCopy = gson.fromJson(gson.toJson(pm), User.class);
    address.setCountry("US");
    assertThat(deepCopy.getAddress().getCountry())
      .isNotEqualTo(pm.getAddress().getCountry());
}

6.3. 使用 Jackson 实现 JSON 序列化

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

让我们看一个例子:

@Test
public void test7() 
  throws IOException {
    Address address = new Address("天府路", "广州", "中国");
    User pm = new User("cuihua", "manager", address);
    ObjectMapper objectMapper = new ObjectMapper();
    
    User deepCopy = objectMapper
      .readValue(objectMapper.writeValueAsString(pm), User.class);
    address.setCountry("US");
    assertThat(deepCopy.getAddress().getCountry())
      .isNotEqualTo(pm.getAddress().getCountry());
}

七、总结一下

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

目录
相关文章
|
6天前
|
安全 Java 编译器
java中类与对象回顾总结-2
java中类与对象回顾总结
20 3
|
6天前
|
Java 编译器
java中类与对象回顾总结-1
java中类与对象回顾总结
16 3
|
6天前
|
消息中间件 Java RocketMQ
MQ产品使用合集之在同一个 Java 进程内建立三个消费对象并设置三个消费者组订阅同一主题和标签的情况下,是否会发生其中一个消费者组无法接收到消息的现象
消息队列(MQ)是一种用于异步通信和解耦的应用程序间消息传递的服务,广泛应用于分布式系统中。针对不同的MQ产品,如阿里云的RocketMQ、RabbitMQ等,它们在实现上述场景时可能会有不同的特性和优势,比如RocketMQ强调高吞吐量、低延迟和高可用性,适合大规模分布式系统;而RabbitMQ则以其灵活的路由规则和丰富的协议支持受到青睐。下面是一些常见的消息队列MQ产品的使用场景合集,这些场景涵盖了多种行业和业务需求。
13 1
|
6天前
|
Java 编译器
【Java开发指南 | 第一篇】类、对象基础概念及Java特征
【Java开发指南 | 第一篇】类、对象基础概念及Java特征
12 4
|
6天前
|
安全 Java 数据安全/隐私保护
Java一分钟之-Java反射机制:动态操作类与对象
【5月更文挑战第12天】本文介绍了Java反射机制的基本用法,包括获取Class对象、创建对象、访问字段和调用方法。同时,讨论了常见的问题和易错点,如忽略访问权限检查、未捕获异常以及性能损耗,并提供了相应的避免策略。理解反射的工作原理和合理使用有助于提升代码灵活性,但需注意其带来的安全风险和性能影响。
24 4
|
6天前
|
Java
【JAVA基础篇教学】第五篇:Java面向对象编程:类、对象、继承、多态
【JAVA基础篇教学】第五篇:Java面向对象编程:类、对象、继承、多态
|
6天前
|
缓存 Java 程序员
关于创建、销毁对象⭐Java程序员需要掌握的8个编程好习惯
关于创建、销毁对象⭐Java程序员需要掌握的8个编程好习惯
关于创建、销毁对象⭐Java程序员需要掌握的8个编程好习惯
|
6天前
|
Java
从源码出发:JAVA中对象的比较
从源码出发:JAVA中对象的比较
20 3
|
6天前
|
Java
Java一分钟之-类与对象:面向对象编程入门
【5月更文挑战第8天】本文为Java面向对象编程的入门指南,介绍了类与对象的基础概念、常见问题及规避策略。文章通过代码示例展示了如何定义类,包括访问修饰符的适当使用、构造器的设计以及方法的封装。同时,讨论了对象创建与使用时可能遇到的内存泄漏、空指针异常和数据不一致等问题,并提供了相应的解决建议。学习OOP需注重理论与实践相结合,不断编写和优化代码。
31 1
|
6天前
|
SQL Java 数据库连接
15:MyBatis对象关系与映射结构-Java Spring
15:MyBatis对象关系与映射结构-Java Spring
31 4