Java 对象进行深拷贝

news/2025/1/31 6:43:51/文章来源:https://www.cnblogs.com/TMesh/p/18693365

目录
  • 拷贝对象
  • 方法一 构造函数
    • 测试用例
  • 方法二 重载clone()方法
    • 重写代码
    • 测试用例
  • 方法三 Apache Commons Lang序列化
    • 重写代码
    • 测试用例
  • 方法四 Gson序列化
    • 测试用例
  • 方法五 Jackson序列化
    • 重写代码
    • 测试用例
  • 总结

在 Java 语言里,当我们需要拷贝一个对象时,有两种类型的拷贝:浅拷贝与深拷贝。浅拷贝只是拷贝了源对象的地址,所以源对象的值发生变化时,拷贝对象的值也会发生变化。而深拷贝则是拷贝了源对象的所有值,所以即使源对象的值发生变化时,拷贝对象的值也不会改变。如下图描述:
[copy1.png]
了解了浅拷贝和深拷贝的区别之后,本篇博客将教大家几种深拷贝的方法。

拷贝对象

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

/**  * 用户  */  
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一个。

测试用例

@Test  
public void constructorCopy() {  Address address = new Address("安徽", "中国");  User user = new User("TM", 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接口,使其支持深拷贝。

/**  * 地址  */  
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();  }  
}
/**  * 用户  */  
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()重新赋值。

测试用例

@Test  
public void cloneCopy() throws CloneNotSupportedException {  Address address = new Address("安徽", "中国");  User user = new User("TM", 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接口,使其支持序列化。

/**  * 地址  */  
public class Address implements Serializable {  private String city;  private String country;  // constructors, getters and setters 
}
/**  * 用户  */  
public class User implements Serializable {  private String name;  private Address address;  // constructors, getters and setters }

测试用例

@Test  
public void serializableCopy() {  Address address = new Address("安徽", "中国");  User user = new User("TM", address);  // 使用Apache Commons Lang序列化进行深拷贝  User copyUser = (User) SerializationUtils.clone(user);  // 修改源对象的值  user.getAddress().setCity("合肥");  // 检查两个对象的值不同  assertNotSame(user.getAddress().getCity(), copyUser.getAddress().getCity());  
}

方法四 Gson序列化

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

测试用例

@Test  
public void gsonCopy() {  Address address = new Address("安徽", "中国");  User user = new User("TM", 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。

/**  * 用户  */  
public class User {  private String name;  private Address address;  // constructors, getters and setters       
public User() {  }
}
/**  * 地址  */  
public class Address {  private String city;  private String country;  // constructors, getters and setters       
public Address() {  }
}

测试用例

@Test  
public void jacksonCopy() throws IOException {  Address address = new Address("安徽", "中国");  User user = new User("TM", 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 CommonsLang序列化 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.序列化与反序列化存在一定的系统开

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/876524.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Java 流程控制

目录概述顺序结构判断语句判断语句1--if判断语句2--if...else判断语句3--if..else if...else选择语句选择语句--switchcase 的穿透性循环语句循环概述循环语句1--for循环语句2--while循环语句3--do...while循环语句的区别跳出语句breakcontinue扩展死循环嵌套循环 概述 在一个程…

Java 面向对象思想

目录面向对象思想概述举例特点类和对象什么是类什么是对象类与对象的关系类的定义事物与类的对比类的定义格式对象的使用对象的使用格式成员变量的默认值对象内存图一个对象,调用一个方法内存图两个对象,调用同一方法内存图一个引用,作为参数传递到方法中内存图成员变量和局…

[USACO 2025 January Contest, Bronze] T3题解

Cow Checkups 题解 题目大意: 对于每一个 \(c=0…n\),求出区间 \([l, r]\) 能使得翻转此区间后满足 \(\sum_{i=1}^n (a_i = b_i)\) 的值恰好为 \(c\) 的数量。 解法: 首先,我们定义两个二维数组:\(s_{l, r}\) : 以 \(l\) 为中点 左右长度为 \(r\),即区间 \([l - r, l + r…

02. Linux的基本操作

一、开启、关闭、重启和查看某个服务我们可以通过如下命令 开启、关闭、重启、查看某个服务。 sudo systemctl start | stop | restart | status 服务名如果我们可以通过查看 /usr/lib/systemd/system 目录下的文件列表来查看有哪些服务,该目录下每个文件都对应一个服务。 ls…

恭祝大家新春快乐!巳巳如意!

欢声笑语除夕夜,万家灯火庆新年, 巳蛇迎春辞旧岁,合家幸福永团圆。

寒假修行2

学了标题最多6个依次变小 以及标题的位置 添加属性 align="left | center |right" 是段落 在中间加或是换行 创建一条水平线 颜色为红色 300宽度 20高度 align默认居中 也可左右 图片 将图片保存后 src为路径 alt规定图像的替代文本 width为宽度 height为高度…

【编码】自定义通信协议——支持更多请求类型

前言 上一篇随笔"如何实现一套自定义网络协议",介绍了自定义协议的粘拆包的处理,消息体的编解码等。 其中包含一个案例,演示怎么实现一个RPC实现。 不过案例中的Request格式是固定的,实际应用中,RPC协议需要支持不同的API。 所以需要有一个字段来表示API类型,而…

Android Qcom board-id加载镜像学习

很早就听说过board-id能用来区分项目,没负责过这个,也一直没有时间去了解。board-id的可以通过gpio或者eeprom来存放,board-id也就是CDT中的部分内容,如果时gpio的方式,可配置的项目有些而且在主板上的都是hardcode,这样不利于维护。 XBL-CDT default: BOOT.XF.4.1/boot_i…

阿里云2025年免费领取300元无门槛优惠券

综合传送门详情免责声明 版权声明 交流群 公众hao服务器有什么用 服务器可以用于管理网络资源,比如控制网络访问、发送/接收电子邮件和 托管网站。服务器用于网站和大型数据库等应用,具有高速计算能力、长期可靠运行、强大的数据吞吐量、高可用性、可靠性、可扩展性和可管理性…

美团面试:MySQL为什么 不用 Docker部署?

本文原文链接 文章很长,且持续更新,建议收藏起来,慢慢读!疯狂创客圈总目录 博客园版 为您奉上珍贵的学习资源 : 免费赠送 :《尼恩Java面试宝典》 持续更新+ 史上最全 + 面试必备 2000页+ 面试必备 + 大厂必备 +涨薪必备 免费赠送 :《尼恩技术圣经+高并发系列PDF》 ,帮你 …

Java 异常

目录异常介绍异常概念异常体系异常分类异常的产生过程解析异常的处理抛出异常 throwObjects 非空判断声明异常 throws捕获异常 try…catchfinally 代码块异常注意事项自定义异常概述例 异常介绍 异常概念 异常,就是不正常的意思。在生活中:医生说,你的身体某个部位有异常,该部…