干货文:如何在 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());
}

七、总结一下

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

目录
相关文章
|
10天前
|
安全 Java 编译器
Java对象一定分配在堆上吗?
本文探讨了Java对象的内存分配问题,重点介绍了JVM的逃逸分析技术及其优化策略。逃逸分析能判断对象是否会在作用域外被访问,从而决定对象是否需要分配到堆上。文章详细讲解了栈上分配、标量替换和同步消除三种优化策略,并通过示例代码说明了这些技术的应用场景。
Java对象一定分配在堆上吗?
|
14天前
|
Java API
Java 对象释放与 finalize 方法
关于 Java 对象释放的疑惑解答,以及 finalize 方法的相关知识。
35 17
|
14天前
|
存储 安全 Java
Java编程中的对象序列化与反序列化
【10月更文挑战第22天】在Java的世界里,对象序列化和反序列化是数据持久化和网络传输的关键技术。本文将带你了解如何在Java中实现对象的序列化与反序列化,并探讨其背后的原理。通过实际代码示例,我们将一步步展示如何将复杂数据结构转换为字节流,以及如何将这些字节流还原为Java对象。文章还将讨论在使用序列化时应注意的安全性问题,以确保你的应用程序既高效又安全。
|
23天前
|
存储 Java 数据管理
Java零基础-Java对象详解
【10月更文挑战第7天】Java零基础教学篇,手把手实践教学!
23 6
|
26天前
|
Oracle Java 关系型数据库
重新定义 Java 对象相等性
本文探讨了Java中的对象相等性问题,包括自反性、对称性、传递性和一致性等原则,并通过LaptopCharger类的例子展示了引用相等与内容相等的区别。文章还介绍了如何通过重写`equals`方法和使用`Comparator`接口来实现更复杂的相等度量,以满足特定的业务需求。
17 3
|
27天前
|
存储 Java
Java编程中的对象序列化与反序列化
【10月更文挑战第9天】在Java的世界里,对象序列化是连接数据持久化与网络通信的桥梁。本文将深入探讨Java对象序列化的机制、实践方法及反序列化过程,通过代码示例揭示其背后的原理。从基础概念到高级应用,我们将一步步揭开序列化技术的神秘面纱,让读者能够掌握这一强大工具,以应对数据存储和传输的挑战。
|
28天前
|
存储 Java 数据管理
Java零基础-Java对象详解
【10月更文挑战第3天】Java零基础教学篇,手把手实践教学!
12 1
|
1月前
|
Java 数据安全/隐私保护
java类和对象
java类和对象
23 5
|
13天前
|
存储 缓存 NoSQL
一篇搞懂!Java对象序列化与反序列化的底层逻辑
本文介绍了Java中的序列化与反序列化,包括基本概念、应用场景、实现方式及注意事项。序列化是将对象转换为字节流,便于存储和传输;反序列化则是将字节流还原为对象。文中详细讲解了实现序列化的步骤,以及常见的反序列化失败原因和最佳实践。通过实例和代码示例,帮助读者更好地理解和应用这一重要技术。
10 0
|
25天前
|
存储 前端开发 Java
你还没有对象吗?java带你创建一个吧
你还没有对象吗?java带你创建一个吧
11 0
下一篇
无影云桌面