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 ...
随机推荐
- 深入理解java虚拟机之垃圾收集器
Java一个重要的优势就是通过垃圾管理器GC (Garbage Collection)自动管理和回收内存,程序员无需通过调用方法来释放内存.也因此很好多的程序员可能会认为Java程序不会出现内存泄漏的 ...
- Android ROM包定制(解包,增删模块,打包)
1. 解包 假设有一个名为update.zip的ROM包,我们要在Ubuntu下对它进行定制.首先把system.transfer.list和system.new.dat.br(有些旧版的系统的镜像可 ...
- Robot Framework源码解析(2) - 执行测试的入口点
我们再来看 src/robot/run.py 的工作原理.摘录部分代码: from robot.conf import RobotSettings from robot.model import Mo ...
- Docker 堆栈
1. Stack stack(译:堆叠,堆栈)是一组相互关联的服务,它们共享依赖关系,并且可以一起编排和伸缩. 在上一篇<Docker 服务>中我们知道可以通过创建一个docker-co ...
- Python中使用面状矢量裁剪栅格影像,并依据Value值更改矢量属性
本文整体思路:在Python中使用Geopandas库,依次读取shp文件的每一个面状要素,获取其空间边界信息并裁剪对应的栅格影像,计算所裁剪影像Value值的众数,将其设置为对应面状要素的NewTY ...
- d3实现家族树
1. jQuery和CSS3支持移动手机的DOM元素移动和缩放插件:panzoom 2.拖动:jqueryUI-Draggable.touchpunch 3.图表:echart.heig ...
- C#编写扫雷游戏
翻看了下以前大学学习的一些小项目,突然发现有个项目比较有意思,觉得有必要把它分享出来.当然现在看来,里面有很多的不足之处,但因博主现在已经工作,没有时间再去优化.这个项目就是利用C#编写一个Windo ...
- 设计模式之过滤器模式——Java语言描述
过滤器模式允许开发人员使用不同的标准来过滤一组对象,通过逻辑运算以解耦的方式把它们连接起来 实现 创建一个Person对象.Criteria 接口和实现了该接口的实体类,来过滤 Person 对象的列 ...
- ArcGIS对SLD样式的支持
文章版权由作者李晓晖和博客园共有,若转载请于明显处标明出处:http://www.cnblogs.com/naaoveGIS/ 1.ArcGISWMS服务对SLD的支持 在完成用geoserver的w ...
- andorid 应用第二次登录实现自动登录
前置条件是所有用户相关接口都走 https,非用户相关列表类数据走 http. 步骤 第一次登陆 getUserInfo 里带有一个长效 token,该长效 token 用来判断用户是否登陆和换取短 ...