首页 > 编程语言 >[转]Java如何对一个对象进行深拷贝

[转]Java如何对一个对象进行深拷贝

时间:2023-06-26 09:25:38浏览次数:38  
标签:Java 对象 address User Address 拷贝 序列化 user

介绍

在Java语言里,当我们需要拷贝一个对象时,有两种类型的拷贝:浅拷贝与深拷贝。浅拷贝只是拷贝了源对象的地址,所以源对象的值发生变化时,拷贝对象的值也会发生变化。而深拷贝则是拷贝了源对象的所有值,所以即使源对象的值发生变化时,拷贝对象的值也不会改变。如下图描述:

了解了浅拷贝和深拷贝的区别之后,本篇博客将教大家几种深拷贝的方法。


拷贝对象

首先,我们定义一下需要拷贝的简单对象。

  arduino 复制代码
/**
 * 用户
 */
public class User {

    private String name;
    private Address address;

    // constructors, getters and setters

}

/**
 * 地址
 */
public class Address {

    private String city;
    private String country;

    // constructors, getters and setters

}

如上述代码,我们定义了一个User用户类,包含name姓名,和address地址,其中address并不是字符串,而是另一个Address类,包含country国家和city城市。构造方法和成员变量的get()、set()方法此处我们省略不写。接下来我们将详细描述如何深拷贝User对象。


方法一 构造函数

我们可以通过在调用构造函数进行深拷贝,形参如果是基本类型和字符串则直接赋值,如果是对象则重新new一个。

测试用例

  scss 复制代码
@Test
public void constructorCopy() {

    Address address = new Address("杭州", "中国");
    User user = new User("大山", address);

    // 调用构造函数时进行深拷贝
    User copyUser = new User(user.getName(), new Address(address.getCity(), address.getCountry()));

    // 修改源对象的值
    user.getAddress().setCity("深圳");

    // 检查两个对象的值不同
    assertNotSame(user.getAddress().getCity(), copyUser.getAddress().getCity());

}

方法二 重载clone()方法

Object父类有个clone()的拷贝方法,不过它是protected类型的,我们需要重写它并修改为public类型。除此之外,子类还需要实现Cloneable接口来告诉JVM这个类是可以拷贝的。

重写代码

让我们修改一下User类,Address类,实现Cloneable接口,使其支持深拷贝。

  java 复制代码
/**
 * 地址
 */
public class Address implements Cloneable {

    private String city;
    private String country;

    // constructors, getters and setters

    @Override
    public Address clone() throws CloneNotSupportedException {
        return (Address) super.clone();
    }

}
  java 复制代码
/**
 * 用户
 */
public class User implements Cloneable {

    private String name;
    private Address address;

    // constructors, getters and setters

    @Override
    public User clone() throws CloneNotSupportedException {
        User user = (User) super.clone();
        user.setAddress(this.address.clone());
        return user;
    }

}

需要注意的是,super.clone()其实是浅拷贝,所以在重写User类的clone()方法时,address对象需要调用address.clone()重新赋值。

测试用例

  java 复制代码
@Test
public void cloneCopy() throws CloneNotSupportedException {

    Address address = new Address("杭州", "中国");
    User user = new User("大山", address);

    // 调用clone()方法进行深拷贝
    User copyUser = user.clone();

    // 修改源对象的值
    user.getAddress().setCity("深圳");

    // 检查两个对象的值不同
    assertNotSame(user.getAddress().getCity(), copyUser.getAddress().getCity());

}

方法三 Apache Commons Lang序列化

Java提供了序列化的能力,我们可以先将源对象进行序列化,再反序列化生成拷贝对象。但是,使用序列化的前提是拷贝的类(包括其成员变量)需要实现Serializable接口。Apache Commons Lang包对Java序列化进行了封装,我们可以直接使用它。

重写代码

让我们修改一下User类,Address类,实现Serializable接口,使其支持序列化。

  arduino 复制代码
/**
 * 地址
 */
public class Address implements Serializable {

    private String city;
    private String country;

    // constructors, getters and setters

}
  arduino 复制代码
/**
 * 用户
 */
public class User implements Serializable {

    private String name;
    private Address address;

    // constructors, getters and setters

}

测试用例

  scss 复制代码
@Test
public void serializableCopy() {

    Address address = new Address("杭州", "中国");
    User user = new User("大山", address);

    // 使用Apache Commons Lang序列化进行深拷贝
    User copyUser = (User) SerializationUtils.clone(user);

    // 修改源对象的值
    user.getAddress().setCity("深圳");

    // 检查两个对象的值不同
    assertNotSame(user.getAddress().getCity(), copyUser.getAddress().getCity());

}

方法四 Gson序列化

Gson可以将对象序列化成JSON,也可以将JSON反序列化成对象,所以我们可以用它进行深拷贝。

测试用例

  java 复制代码
@Test
public void gsonCopy() {

    Address address = new Address("杭州", "中国");
    User user = new User("大山", address);

    // 使用Gson序列化进行深拷贝
    Gson gson = new Gson();
    User copyUser = gson.fromJson(gson.toJson(user), User.class);

    // 修改源对象的值
    user.getAddress().setCity("深圳");

    // 检查两个对象的值不同
    assertNotSame(user.getAddress().getCity(), copyUser.getAddress().getCity());

}

方法五 Jackson序列化

Jackson与Gson相似,可以将对象序列化成JSON,明显不同的地方是拷贝的类(包括其成员变量)需要有默认的无参构造函数。

重写代码

让我们修改一下User类,Address类,实现默认的无参构造函数,使其支持Jackson。

  arduino 复制代码
/**
 * 用户
 */
public class User {

    private String name;
    private Address address;

    // constructors, getters and setters

    public User() {
    }

}
  arduino 复制代码
/**
 * 地址
 */
public class Address {

    private String city;
    private String country;

    // constructors, getters and setters

    public Address() {
    }

}

测试用例

  java 复制代码
@Test
public void jacksonCopy() throws IOException {

    Address address = new Address("杭州", "中国");
    User user = new User("大山", address);

    // 使用Jackson序列化进行深拷贝
    ObjectMapper objectMapper = new ObjectMapper();
    User copyUser = objectMapper.readValue(objectMapper.writeValueAsString(user), User.class);

    // 修改源对象的值
    user.getAddress().setCity("深圳");

    // 检查两个对象的值不同
    assertNotSame(user.getAddress().getCity(), copyUser.getAddress().getCity());

}

总结

说了这么多深拷贝的实现方法,哪一种方法才是最好的呢?最简单的判断就是根据拷贝的类(包括其成员变量)是否提供了深拷贝的构造函数、是否实现了Cloneable接口、是否实现了Serializable接口、是否实现了默认的无参构造函数来进行选择。如果需要详细的考虑,则可以参考下面的表格:

深拷贝方法优点缺点
构造函数 1. 底层实现简单
2. 不需要引入第三方包
3. 系统开销小
4. 对拷贝类没有要求,不需要实现额外接口和方法
1. 可用性差,每次新增成员变量都需要新增新的拷贝构造函数
重载clone()方法 1. 底层实现较简单
2. 不需要引入第三方包
3. 系统开销小
1. 可用性较差,每次新增成员变量可能需要修改clone()方法
2. 拷贝类(包括其成员变量)需要实现Cloneable接口
Apache Commons Lang序列化 1. 可用性强,新增成员变量不需要修改拷贝方法 1. 底层实现较复杂
2. 需要引入Apache Commons Lang第三方JAR包
3. 拷贝类(包括其成员变量)需要实现Serializable接口
4. 序列化与反序列化存在一定的系统开销
Gson序列化 1. 可用性强,新增成员变量不需要修改拷贝方法
2. 对拷贝类没有要求,不需要实现额外接口和方法
1. 底层实现复杂
2. 需要引入Gson第三方JAR包
3. 序列化与反序列化存在一定的系统开销
Jackson序列化 1. 可用性强,新增成员变量不需要修改拷贝方法 1. 底层实现复杂
2. 需要引入Jackson第三方JAR包
3. 拷贝类(包括其成员变量)需要实现默认的无参构造函数
4. 序列化与反序列化存在一定的系统开销

参考阅读

[1] How to Make a Deep Copy of an Object in Java


作者:wudashan
链接:https://juejin.cn/post/6844903693100417038
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

标签:Java,对象,address,User,Address,拷贝,序列化,user
From: https://www.cnblogs.com/dirgo/p/17504468.html

相关文章

  • Lazy 延迟初始化对象
    什么是Lazy:    在C#中,Lazy类是用来延迟初始化对象的一种机制。它允许你在第一次使用该对象之前推迟其创建过程,以提高性能和资源利用率。使用Lazy类可以在需要时才创建对象,并确保只创建一次使用示例:   ......
  • Java学习笔记(十六)
    1.什么是线程?线程是指操作系统中的一种执行单元,它是进程中的一部分,可以看作是轻量级的进程。与进程不同的是,线程共享同一进程的地址空间和系统资源,如打开文件和信号处理等,但每个线程都有自己的程序计数器(PC)和栈,用于执行代码和存储局部变量等数据。2。线程和进程有什么区别?线......
  • JAVA方法
    java方法原子性​​publicstaticintmax(intnum1,intnum2){  intresult=0;//初始化  if(num1==num2){    System.out.println("num1==num2");    return0;//终止方法 }  if(num1>num2){    result=num1; }else{   ......
  • 6月25日java学习日记
    端午节小休息了几天,断开连接了几天,希望快速状态回到吧,今天了解了部分java异常类,Exceptions类为异常类,学习了throw关键字,以及了解了trycatch的用法(基本与C#相同),同时使用HasgMap以及List.of方法实现了斗地主案例。 ......
  • 学习Java前的一些介绍
    1.java开发环境搭建卸载:删除Java的安装目录删除JAVA_HOME删除path下关于java的目录java-version验证安装:创建jdk和jre两个目录(还可以创建一个code目录存放代码)百度搜索jdk(建议安装jdk8)同意协议,找到对应版本并且下载双击安装jdk配置环境变量我......
  • springboot中自定义JavaBean返回的json对象属性名称大写变小写问题
    目录一、继承类二、手动添加Get方法三、@JsonProperty四、spring-bootjson(jackson)属性命名策略开发过程中发现查询返回的数据出现自定义的JavaBean的属性值大小写格式出现问题,导致前端无法接受到数据,目前有四种解决方法,根据大佬的经验之谈,前两种是最简单便捷的,后两种是比较通......
  • 面向过程概念 面向对象概念 类的定义和对象的产生 对象独有的数据 属性的查找顺序
    目录面向过程概念面向对象概念类的定义和对象的产生对象独有的数据属性的查找顺序面向过程概念面向过程(ProcedureOriented)是一种以过程为中心的编程思想。这些都是以什么正在发生为主要目标进行编程,不同于面向对象的是谁在受影响。与面向对象明显的不同就是封装、继承、类......
  • ActiveX 控件在过去是非常流行的技术,但近年来已经逐渐被其他技术所取代。由于其局限性
    ActiveX控件是一种可重用的软件组件,它们基于微软的COM(ComponentObjectModel)技术,并被广泛应用于Windows平台上的应用程序开发。ActiveX控件可以包含图形用户界面元素、功能模块、数据处理等,并提供给其他应用程序使用。下面是关于ActiveX控件的一些常见信息:安装和注册:使用A......
  • OLE(Object Linking and Embedding)是一种由Microsoft开发的技术,用于在Windows操作系统
    OLE(ObjectLinkingandEmbedding)是一种由Microsoft开发的技术,用于在Windows操作系统中实现对象链接和嵌入。它允许应用程序在同一文档或不同文档之间共享和操作对象的数据。使用OLE,应用程序可以将一个对象插入到另一个应用程序中,并且这个对象仍然保持其源应用程序中的特性和功能......
  • Java基础-Day02
    Java基础-Day02运算符算法运算符​赋值运算符shorts1=10;s1=s1+2;//编译失败s1+=2;//编译成功,结论:不会改变变量本身的数据类型(推荐使用)比较运算符特别说明:比较运算符的结果是boolean类型<,>,<=,>=,!=:只能使用在数值类型的数据之间==......