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 ...
随机推荐
- 解决:git push error: failed to push some refs to
出现错误的原因是github中的README.md文件不在本地代码目录中. 也就是说我们需要先将远程代码库中的任何文件先pull到本地代码库中,才能push新的代码到github代码库中. 使用如下命 ...
- MySQL 复制 - 性能与扩展性的基石 3:常见问题及解决方案
主备复制过程中有很大可能会出现各种问题,接下来我们就讨论一些比较普遍的问题,以及当遇到这些问题时,如何解决或者预防问题发生. 1 数据损坏或丢失 问题描述:服务器崩溃.断电.磁盘损坏.内存或网络错误等 ...
- 解决VS2019中.net core WPF 暂时无法使用 Designer 的临时方法
目录 解决 VS2019 中.net core WPF 暂时无法使用 Designer 的临时方法 安装 vs 2019 professional/enterprise版本 在vs的设置里,勾选.NE ...
- win10安装gitLab
从控制面板选择hyper-V进行安装 1.打开控制面板选择程序=>选择启用或关闭windows功能=>选择Hyper-v 安装ubuntu 1.下载ubuntu系统(本次安装为18.04. ...
- Odoo:全球第一免费开源ERP 人力资源模块操作指南(完美珍藏版)
概述 人力资源管理概述 一般企业里,和人力资源相关的工作有:1)员工合同管理,即员工基本档案管理:2)招聘管理,即岗位及岗位人员补充管理:3)员工薪资计算: 4)员工考勤:5)员工休假管理:6)员工绩 ...
- DokuWiki的发现之旅
★DokuWiki介绍 Wiki是什么?是一种允许一群用户用简单的描述来创建和连接一组网页的社会计算系统,可以让人们在web的基础上对Wiki文本进行浏览.创建和更改,是一种人类的知识的网络系统,有助 ...
- github常见操作和常见错误!错误提示:fatal: remote origin already exist
如果输入$ git remote add origin git@github.com:djqiang(github帐号名)/gitdemo(项目名).git 提示出错信息:fatal: remote ...
- Windows 安装 Scoop
Scoop介绍 scoop是Windows下的包管理工具 安装环境要求 1,操作环境:win10 2,确保你的 PowerShell 版本 >= 3. win7或许低于3,得升级.如何确认Pow ...
- 使用Chrome 中的 ssh 插件登陆 linux 服务器
. 在chrome 商店中搜索 secure shell , 安装 Secure Shell Extension 2. 安装完成后,就可以在应用程式中找到Secure Shell App 3.点击 S ...
- 终于等到你:CYQ.Data V5系列 (ORM数据层,支持.NET Core)最新版本开源了
前言: 不要问我框架为什么从收费授权转到免费开源,人生没有那么多为什么,这些年我开源的东西并不少,虽然这个是最核心的,看淡了就也没什么了. 群里的网友:太平说: 记得一年前你开源另一个项目的时候我就说 ...