java深克隆和浅克隆

基本概念

  1. 浅复制(浅克隆)

被复制对象的所有变量都含有与原来的对象相同的值,而所有的对其他对象的引用仍然指向原来的对象。换言之,浅复制仅仅复制所拷贝的对象,而不复制它所引用的对象。

  1. 深复制(深克隆)

    被复制对象的所有变量都含有与原来的对象相同的值,除去那些引用其他对象的变量。那些引用其他对象的变量将指向被复制过的新对象,而不再是原有的那些被引用的对象。换言之,深复制把要复制的对象所引用的对象都复制了一遍。

实现java深复制和浅复制的最关键的就是要实现Object中的clone()方法


如何使用clone()方法

首先我们来看一下Cloneable接口:

官方解释:

1:实现此接口则可以使用java.lang.Object 的clone()方法,否则会抛出CloneNotSupportedException 异常

2:实现此接口的类应该使用公共方法覆盖clone方法

3:此接口并不包含clone 方法,所以实现此接口并不能克隆对象,这只是一个前提,还需覆盖上面所讲的clone方法。

public interface Cloneable {
}

看看Object里面的Clone()方法:

  1. clone()方法返回的是Object类型,所以必须强制转换得到克隆后的类型
  2. clone()方法是一个native方法,而native的效率远远高于非native方法,
  3. 可以发现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异常, 官方解释如下:

  1. 对象的类不支持Cloneable接口
  2. 覆盖方法的子类也可以抛出此异常表示无法克隆实例。

所以我们更改代码如下:

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方法的约定

  1. 对任何的对象x,都有x.clone() !=x 因为克隆对象与原对象不是同一个对象
  2. 对任何的对象x,都有x.clone().getClass()= =x.getClass()//克隆对象与原对象的类型一样
  3. 如果对象x的equals()方法定义恰当,那么x.clone().equals(x)应该成立

对于以上三点要注意,这3项约定并没有强制执行,所以如果用户不遵循此约定,那么将会构造出不正确的克隆对象,所以根据effective java的建议:

谨慎的使用clone方法,或者尽量避免使用。


浅复制实例

  1. 对象中全部是基本类型
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

从运行结果和图上可以知道,克隆后的值变量会开辟新的内存地址,克隆对象修改值不会影响原来对象。

  1. 对象中含有引用类型
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修饰的引用变量)引用类型的成员变量都开辟内存空间所以一般深拷贝对于浅拷贝来说是比较耗费时间和内存开销的。

深拷贝的两种方式:

  1. 重写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

  1. 序列化实现深克隆

我们发现上面通过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方法

    参考以下回答:

https://www.zhihu.com/question/52490586


java 深克隆(深拷贝)与浅克隆(拷贝)详解的更多相关文章

  1. java 深拷贝与浅拷贝机制详解

    概要: 在Java中,拷贝分为深拷贝和浅拷贝两种.java在公共超类Object中实现了一种叫做clone的方法,这种方法clone出来的新对象为浅拷贝,而通过自己定义的clone方法为深拷贝. (一 ...

  2. 零拷贝详解 Java NIO学习笔记四(零拷贝详解)

    转 https://blog.csdn.net/u013096088/article/details/79122671 Java NIO学习笔记四(零拷贝详解) 2018年01月21日 20:20:5 ...

  3. java的集合框架最全详解

    java的集合框架最全详解(图) 前言:数据结构对程序设计有着深远的影响,在面向过程的C语言中,数据库结构用struct来描述,而在面向对象的编程中,数据结构是用类来描述的,并且包含有对该数据结构操作 ...

  4. Java学习-007-Log4J 日志记录配置文件详解及实例源代码

    此文主要讲述在初学 Java 时,常用的 Log4J 日志记录配置文件详解及实例源代码整理.希望能对初学 Java 编程的亲们有所帮助.若有不足之处,敬请大神指正,不胜感激!源代码测试通过日期为:20 ...

  5. 【转】Java魔法堂:String.format详解

    Java魔法堂:String.format详解     目录     一.前言    二.重载方法     三.占位符     四.对字符.字符串进行格式化     五.对整数进行格式化     六. ...

  6. java线程池的使用与详解

    java线程池的使用与详解 [转载]本文转载自两篇博文:  1.Java并发编程:线程池的使用:http://www.cnblogs.com/dolphin0520/p/3932921.html   ...

  7. Java之Static静态修饰符详解

    Java之Static静态修饰符详解 Java之Static静态修饰符详解 一.特点 1.随着类的加载而加载,随着类的消失而消失,生命周期最长 2.优先于对象存在 3.被所有类的对象共享 4.可以直接 ...

  8. Java 反射 设计模式 动态代理机制详解 [ 转载 ]

    Java 反射 设计模式 动态代理机制详解 [ 转载 ] @author 亦山 原文链接:http://blog.csdn.net/luanlouis/article/details/24589193 ...

  9. Java线程创建形式 Thread构造详解 多线程中篇(五)

    Thread作为线程的抽象,Thread的实例用于描述线程,对线程的操纵,就是对Thread实例对象的管理与控制. 创建一个线程这个问题,也就转换为如何构造一个正确的Thread对象. 构造方法列表 ...

  10. (7)Java数据结构--集合map,set,list详解

    MAP,SET,LIST,等JAVA中集合解析(了解) - clam_clam的专栏 - CSDN博---有颜色, http://blog.csdn.net/clam_clam/article/det ...

随机推荐

  1. 测试修改hosts文件py小工具

    import sys,osparm_list=sys.argvHOST_PATH=r'C:\liuliang\flask_test\test\hosts'class HostFile(object): ...

  2. Java连接redis

    一.依赖包 jedis-2.1.0.jar   commons-pool-1.6.jar 二.实例 //连接参数public class RedisConfig { public static int ...

  3. Python-爬取妹子图(单线程和多线程版本)

    一.参考文章 Python爬虫之——爬取妹子图片 上述文章中的代码讲述的非常清楚,我的基本能思路也是这样,本篇文章中的代码仅仅做了一些异常处理和一些日志显示优化工作,写此文章主要是当做笔记,方便以后查 ...

  4. Asp.Net Core&Jenkins持续交付到Windows Server

    对于Linux平台上的持续集成和持续交付可以使用Docker快捷的完成任务,但是Windows Server下却不好使用,一般来讲,windows Server下最简单的发布方式是直接手动打包发布或是 ...

  5. Java工程师修炼之路(校招总结)

    Java工程师修炼之路(校招总结) 前言 在下本是跨专业渣考研的985渣硕一枚,经历研究生两年的学习积累,有幸于2019秋季招聘中拿到几个公司的研发岗offer,包括百度,阿里,腾讯,今日头条,网易, ...

  6. 包装类及 LeetCode 每日一题

    1.包装类与创建对象 Java 为8大数据类型都提供了相应的包装类,并提供属性和方法,更方便的操作基本数据类型.包装类位于java.lang包中. 对于这几种类型的基本数据,都有相似的方法实现基本数据 ...

  7. forEach循环对集合进行循环时,需判断是否为null;

    分析forEach的源码会发现:foreach源码例子: public class Foreach { public static void main(String[] args) { List< ...

  8. SSM框架视频资料

    SSM框架视频资料 进行过 Java Web 项目开发的同学,可能都知道 SSM 框架,即 Spring + SpringMVC + Mybatis .很多项目的主体框架都是采用这种模式,这也是 Ja ...

  9. 全网首发! Odoo 订单分解执行及供应链管理免费开源解决方案

    引言 前一篇介绍了佛山王朝家具的案例背景.佛山王朝家具公司在全国有30多家门店,三个生产工厂.王朝家具有六大痛点问题: 订单迫切需要实现电子化管理及在线签名 总部分单工作量大,供应链效率低 配送和售后 ...

  10. 【技术讨论】RF环境搭建手册

    (原创文章,转载请注明出处.) 简要整理下环境搭建的步骤,以便快速.准确的搭建测试环境. 一.环境搭建 一.Python 2.7 1. 不要用Python3.6,很多库3.6中还没有,wxPython ...