java 深克隆(深拷贝)与浅克隆(拷贝)详解
java深克隆和浅克隆
基本概念
浅复制(浅克隆)
被复制对象的所有变量都含有与原来的对象相同的值,而所有的对其他对象的引用仍然指向原来的对象。换言之,浅复制仅仅复制所拷贝的对象,而不复制它所引用的对象。
深复制(深克隆)被复制对象的所有变量都含有与原来的对象相同的值,除去那些引用其他对象的变量。那些引用其他对象的变量将指向被复制过的新对象,而不再是原有的那些被引用的对象。换言之,深复制把要复制的对象所引用的对象都复制了一遍。
实现java深复制和浅复制的最关键的就是要实现Object中的clone()方法。
如何使用clone()方法
首先我们来看一下Cloneable接口:
官方解释:
1:实现此接口则可以使用java.lang.Object 的clone()方法,否则会抛出CloneNotSupportedException 异常
2:实现此接口的类应该使用公共方法覆盖clone方法
3:此接口并不包含clone 方法,所以实现此接口并不能克隆对象,这只是一个前提,还需覆盖上面所讲的clone方法。
public interface Cloneable {
}
看看Object里面的Clone()方法:
- clone()方法返回的是Object类型,所以必须强制转换得到克隆后的类型
- clone()方法是一个native方法,而native的效率远远高于非native方法,
- 可以发现clone方法被一个Protected修饰,所以可以知道必须继承Object类才能使用,而Object类是所有类的基类,也就是说所有的类都可以使用clone方法
protected native Object clone() throws CloneNotSupportedException;
小试牛刀:
public class Person {
public void testClone(){
super.clone(); // 报错了
}
}
事实却是clone()方法报错了,那么肯定奇怪了,既然Object是一切类的基类,并且clone的方法是Protected的,那应该是可以通过super.clone()方法去调用的,然而事实却是会抛出CloneNotSupportedException异常, 官方解释如下:
- 对象的类不支持Cloneable接口
- 覆盖方法的子类也可以抛出此异常表示无法克隆实例。
所以我们更改代码如下:
public class Person implements Cloneable{
public void testClone(){
try {
super.clone();
System.out.println("克隆成功");
} catch (CloneNotSupportedException e) {
System.out.println("克隆失败");
e.printStackTrace();
}
}
public static void main(String[] args) {
Person p = new Person();
p.testClone();
}
}
要注意,必须将克隆方法写在try-catch块中,因为clone方法会把异常抛出,当然程序也要求我们try-catch。
java.lang.object规范中对clone方法的约定
对任何的对象x,都有x.clone() !=x 因为克隆对象与原对象不是同一个对象对任何的对象x,都有x.clone().getClass()= =x.getClass()//克隆对象与原对象的类型一样如果对象x的equals()方法定义恰当,那么x.clone().equals(x)应该成立
对于以上三点要注意,这3项约定并没有强制执行,所以如果用户不遵循此约定,那么将会构造出不正确的克隆对象,所以根据effective java的建议:
谨慎的使用clone方法,或者尽量避免使用。
浅复制实例
对象中全部是基本类型
public class Teacher implements Cloneable{
private String name;
private int age;
public Teacher(String name, int age){
this.name = name;
this.age = age;
}
// 覆盖
@Override
public Object clone() {
try {
return super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
return null;
}
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
// 客户端测试
public class test {
Teacher origin = new Teacher("tony", 11);
System.out.println(origin.getName());
Teacher clone = (Teacher) origin.clone();
clone.setName("clone");
System.out.println(origin.getName());
System.out.println(clone.getName());
}
结果:
tony
tony
clone

从运行结果和图上可以知道,克隆后的值变量会开辟新的内存地址,克隆对象修改值不会影响原来对象。
对象中含有引用类型
public class Teacher implements Cloneable{
private String name;
private int age;
private Student student;
public Teacher(String name, int age, Student student){
this.name = name;
this.age = age;
this.student = student;
}
// 覆盖
@Override
public Object clone() {
try {
return super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
return null;
}
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public Student getStudent() {
return student;
}
public void setStudent(Student student) {
this.student = student;
}
}
// 学生类
public class Student {
private String name;
private int age;
public Student(String name, int age){
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
// 客户端测试
public class test {
public static void main(String[] args) {
Student student = new Student("学生1" ,11);
Teacher origin = new Teacher("老师", 11, student);;
Teacher clone = (Teacher) origin.clone();
System.out.println("比较克隆后的引用对象");
System.out.println(origin.getStudent().getClass() == clone.getStudent().getClass());
Student student2 = new Student("学生2", 12);
clone.setStudent(student2);
System.out.println("克隆后,比较克隆对象改变引用");
System.out.println(origin.getStudent().getClass() == clone.getStudent().getClass());
}
}
运行结果:
比较克隆后的引用对象
true
克隆后,比较克隆对象改变引用
true

如图可知,引用类型只会存在一份内存地址,执行object的clone方法拷贝的也是引用的复制(这部分的内存空间不一样,)但是引用指向的内存空间是一样的,原对象修改引用变量或者浅拷贝对象修改引用变量都会引起双方的变化
重点:综上两个方面可以知道,Object的clone方法是属于浅拷贝,基本变量类型会复制相同值,而引用变量类型也是会复制相同的引用。
深复制实例
从上面的浅拷贝可以知道,对于引用的变量只会拷贝引用指向的地址,也就是指向同一个内存地址,但是很多情况下我们需要的是下面图的效果:

深拷贝实现的是对所有可变(没有被final修饰的引用变量)引用类型的成员变量都开辟内存空间所以一般深拷贝对于浅拷贝来说是比较耗费时间和内存开销的。
深拷贝的两种方式:
重写clone方法实现深拷贝
学生类:
public class Student implements Cloneable {
private String name;
private int age;
public Student(String name, int age){
this.name = name;
this.age = age;
}
@Override
protected Object clone() {
try {
return super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
return null;
}
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
老师类:
public class Teacher implements Cloneable{
private String name;
private int age;
private Student student;
public Teacher(String name, int age, Student student){
this.name = name;
this.age = age;
this.student = student;
}
// 覆盖
@Override
public Object clone() {
Teacher t = null;
try {
t = (Teacher) super.clone();
t.student = (Student)student.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return t;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public Student getStudent() {
return student;
}
public void setStudent(Student student) {
this.student = student;
}
}
测试端:
public class test {
public static void main(String[] args) {
Student s = new Student("学生1", 11);
Teacher origin = new Teacher("老师原对象", 23, s);
System.out.println("克隆前的学生姓名:" + origin.getStudent().getName());
Teacher clone = (Teacher) origin.clone();
// 更改克隆后的学生信息 更改了姓名
clone.getStudent().setName("我是克隆对象更改后的学生2");
System.out.println("克隆后的学生姓名:" + clone.getStudent().getName());
}
}
运行结果:
克隆前的学生姓名:学生1
克隆后的学生姓名:我是克隆对象更改后的学生2
序列化实现深克隆
我们发现上面通过object的clone方法去实现深克隆十分麻烦, 因此引出了另外一种方式:序列化实现深克隆。
概念:
- 序列化:把对象写到流里
- 反序列化:把对象从流中读出来
在Java语言里深复制一个对象,常常可以先使对象实现Serializable接口,然后把对象(实际上只是对象的一个拷贝)写到一个流里,再从流里读出来,便可以重建对象。
注意:
写在流里的是对象的一个拷贝,而原对象仍然存在于JVM里面。对象以及对象内部所有引用到的对象都是可序列化的如果不想序列化,则需要使用transient来修饰
案例:
Teacher:
public class Teacher implements Serializable{
private String name;
private int age;
private Student student;
public Teacher(String name, int age, Student student){
this.name = name;
this.age = age;
this.student = student;
}
// 深克隆
public Object deepClone() throws IOException, ClassNotFoundException {
// 序列化
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(this);
// 反序列化
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis);
return ois.readObject();
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public Student getStudent() {
return student;
}
public void setStudent(Student student) {
this.student = student;
}
Student:
public class Student implements Serializable {
private String name;
private int age;
public Student(String name, int age){
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
client:
public class test {
public static void main(String[] args) {
try {
Student s = new Student("学生1", 11);
Teacher origin = new Teacher("老师原对象", 23, s);
System.out.println("克隆前的学生姓名:" + origin.getStudent().getName());
Teacher clone = (Teacher) origin.deepClone();
// 更改克隆后的d学生信息 更改了姓名
clone.getStudent().setName("我是克隆对象更改后的学生2");
System.out.println("克隆后的学生姓名:" + clone.getStudent().getName());
}catch (Exception e){
e.printStackTrace();
}
}
}
当然这些工作都有现成的轮子了,借助于Apache Commons可以直接实现:
- 浅克隆:BeanUtils.cloneBean(Object obj);
- 深克隆:SerializationUtils.clone(T object);
最后探讨
在java中为什么实现了Cloneable接口,就可以调用Object中的Clone方法
参考以下回答:
java 深克隆(深拷贝)与浅克隆(拷贝)详解的更多相关文章
- java 深拷贝与浅拷贝机制详解
概要: 在Java中,拷贝分为深拷贝和浅拷贝两种.java在公共超类Object中实现了一种叫做clone的方法,这种方法clone出来的新对象为浅拷贝,而通过自己定义的clone方法为深拷贝. (一 ...
- 零拷贝详解 Java NIO学习笔记四(零拷贝详解)
转 https://blog.csdn.net/u013096088/article/details/79122671 Java NIO学习笔记四(零拷贝详解) 2018年01月21日 20:20:5 ...
- java的集合框架最全详解
java的集合框架最全详解(图) 前言:数据结构对程序设计有着深远的影响,在面向过程的C语言中,数据库结构用struct来描述,而在面向对象的编程中,数据结构是用类来描述的,并且包含有对该数据结构操作 ...
- Java学习-007-Log4J 日志记录配置文件详解及实例源代码
此文主要讲述在初学 Java 时,常用的 Log4J 日志记录配置文件详解及实例源代码整理.希望能对初学 Java 编程的亲们有所帮助.若有不足之处,敬请大神指正,不胜感激!源代码测试通过日期为:20 ...
- 【转】Java魔法堂:String.format详解
Java魔法堂:String.format详解 目录 一.前言 二.重载方法 三.占位符 四.对字符.字符串进行格式化 五.对整数进行格式化 六. ...
- java线程池的使用与详解
java线程池的使用与详解 [转载]本文转载自两篇博文: 1.Java并发编程:线程池的使用:http://www.cnblogs.com/dolphin0520/p/3932921.html ...
- Java之Static静态修饰符详解
Java之Static静态修饰符详解 Java之Static静态修饰符详解 一.特点 1.随着类的加载而加载,随着类的消失而消失,生命周期最长 2.优先于对象存在 3.被所有类的对象共享 4.可以直接 ...
- Java 反射 设计模式 动态代理机制详解 [ 转载 ]
Java 反射 设计模式 动态代理机制详解 [ 转载 ] @author 亦山 原文链接:http://blog.csdn.net/luanlouis/article/details/24589193 ...
- Java线程创建形式 Thread构造详解 多线程中篇(五)
Thread作为线程的抽象,Thread的实例用于描述线程,对线程的操纵,就是对Thread实例对象的管理与控制. 创建一个线程这个问题,也就转换为如何构造一个正确的Thread对象. 构造方法列表 ...
- (7)Java数据结构--集合map,set,list详解
MAP,SET,LIST,等JAVA中集合解析(了解) - clam_clam的专栏 - CSDN博---有颜色, http://blog.csdn.net/clam_clam/article/det ...
随机推荐
- keras实现简单CNN人脸关键点检测
用keras实现人脸关键点检测 改良版:http://www.cnblogs.com/ansang/p/8583122.html 第一步:准备好需要的库 tensorflow 1.4.0 h5py ...
- im2col:将卷积运算转为矩阵相乘
目录 im2col实现 优缺点分析 参考 博客:blog.shinelee.me | 博客园 | CSDN im2col实现 如何将卷积运算转为矩阵相乘?直接看下面这张图,以下图片来自论文High P ...
- 越来越火的"中台"是什么
很多企业都将促进业务与科技的深度融合作为发展战略,也都想学学阿里的中台战略,其实,除了中台战略之外,基于企业级业务架构设计来实现组件化开发也是企业数字化转型的优选路径,是弥合业务与技术之间“数字鸿沟” ...
- SpringBoot之旅第二篇-配置
一.引言 虽然springboot帮我们进行了自动配置,但配置还是不可避免的,比如最简单的端口号,数据库连接.但springboot的配置一般不用xml进行配置,而是yml和properties,选择 ...
- 程序猿必知必会Linux命令之awk
前言 对于一名专业的程序员来说,Linux相关知识是必须要掌握的,其中对于文本的处理更是我们常见的操作,比如格式化输出我们需要的数据,这些数据可能会来源于文本文件或管道符,或者统计文本里面我们需要的数 ...
- CSharpGL(50)使用Assimp加载骨骼动画
CSharpGL(50)使用Assimp加载骨骼动画 在(http://ogldev.atspace.co.uk/www/tutorial38/tutorial38.html)介绍了C++用Asism ...
- Windows Server 2016-批量设置用户下次登陆须更改密码
Powershell设置某OU下所有用户下次登陆必须更改密码: Get-ADUser -Filter * -SearchBase "ou=syncall,dc=azureyun,dc=com ...
- 【转】Python爬取AES加密的m3u8视频流的小电影并转换成mp4
最近发现一个视频网站,准备去爬取得时候,前面很顺利利用fiddler抓包获取网站的post数据loads为python字典数据,分析数据就能发现每个视频的连接地址就在其中, 发现这些都是m3u8文件流 ...
- .NET 基金会完成第一次全面改选
.NET基金会是一个独立的组织,支持.NET社区和开源,旨在拓宽和加强.NET生态系统和社区.这可以通过多种方式完成,包括项目指导,指导,法律和营销帮助,技术和财务支持设置等,2014年微软组织成立. ...
- zabbix安装及简单使用备注
1.安装mysql yum install -y mariadb mariadb-server systemctl start mariadb 2.安装apache yum -y install ht ...