概述

clone 翻译过来就是 克隆,顾名思义就是创造一个一模一样的事物。Java 代码中 clone() 方法是 Object 方法,而 Object 又是所有类的父类,也就是说所有 java 对象都可以调用 clone 方法克隆一个和自己 “相同” 的对象。本篇博客我打算系统整理 java clone() 方法的原理以及深克隆和浅克隆的关系。


cloneable 接口

在正式介绍 clone() 方法前,我们先简单聊聊 cloneable 接口。在编码过程中,一般都是通过实现该接口重写 clone() 方法,然后就可以调用 clone() 方法执行重写后的逻辑。

需要注意的一点是:Object 类中的 clone() 方法是通过 native 修饰的,也就是说它是通过直接调用底层操作系统方法实现的,默认不重写 clone() 方法时调用 Object 类的 clone() 方法。


clone()

java 对象调用 clone() 方法可以创建一个新的对象。除了调用 clone() 方法外,java 语言还有很多种创建对象的方式,本篇我们主要看 new 关键字clone() 方法

  • new :当我们使用 new 关键字创建对象时,jvm 首先根据关键字后面的类型确定需要申请的内存大小,申请完内存后,执行类的构造方法。在执行构造方法期间,填充内存中各个属性域,这个填充的过程也叫 初始化。构造方法执行完标志着对象创建成功,此时返回对象地址,在栈区以引用的方式调用对象。
  • clone :当我们调用某个对象的 clone() 方法克隆对象时,首先根据原对象的内存大小申请内存空间,申请完内存空间后,将原对象内存域复制到新申请的内存空间,复制完成标志着克隆完成,返回引用类型。

通过上面的描述我们可以看出,new 和 clone() 第一步都是申请内存,只不过 new 关键字通过类构造方法初始化对象,clone() 方法直接通过克隆内存域完成对象创建。


对象和引用

在正式开始介绍 clone() 方法前,我们先简单描述对象和引用的区别:

  • 对象:绝大多数对象在堆区,它是实际保存属性的内存空间
  • 引用:引用大多引用在栈区,可以将它理解为指向实际对象地址的指针

关于对象和引用我简单总结如下:

  • 一个对象可以有多个引用,但一个引用只能指向一个对象
  • 当我们使用 == 比较对象时,一般比较的是对象地址

举个简单的例子:

@Test
public void test1() {ClassRoom c1 = new ClassRoom();ClassRoom c2 = c1;System.out.println(c1);System.out.println(c2);System.out.println(c1 == c2);
}

执行结果

com.qhd.worker.CloneTest$ClassRoom@927a81
com.qhd.worker.CloneTest$ClassRoom@927a81
true

从执行结果可以看出,即使是两个不同的引用,指向相同对象时,他们也是相同的,并且值都是指向对象的地址。简单绘图表示的话,如下所示:

有了上面的基础,我们再来看 clone() 方法克隆:

@Test
public void test2() throws CloneNotSupportedException {ClassRoom c1 = new ClassRoom();ClassRoom c2 = (ClassRoom) c1.clone();System.out.println(c1);System.out.println(c2);System.out.println(c1 == c2);
}

执行结果

com.qhd.worker.clone.CloneTest$ClassRoom@927a81
com.qhd.worker.clone.CloneTest$ClassRoom@e03bb5
false

从执行结果可以看出,此时两个引用指向不同地址。clone() 方法创建了一个新的对象:


深克隆与浅克隆

通过上面的整理可以看出,clone() 方法会创建一个新的对象。下面我们主要看两种克隆的关系:

  • 如果一个对象中所有属性都是基础类型,那么它的深克隆和浅克隆结果完全相同
  • 如果一个对象包含引用类型数据,如果克隆之后的引用所指向对象是不同对象,那么它是深克隆,否则是浅克隆

也就是说:深克隆、浅克隆的划分完全是根据是否克隆引用属性来说的。对于基本类型变量,克隆后数值大小相同即可,但对于引用数据类型,深克隆会复制引用指向的对象,而浅克隆只会克隆引用。

举个例子,假设现在存在如下 class 类:

class ClassRoom implements Cloneable {// 班级人数private int num;// 班级名称private String name;@Overrideprotected Object clone() throws CloneNotSupportedException {return super.clone();}
}

下面我通过抽象图的形式描述深克隆和浅克隆的区别:

通过上图可以明显看出,深克隆时 String 引用对应会被克隆,但浅克隆只会克隆引用,不会克隆具体对象。下面我通过简单的代码测试 clone() 方法默认是深克隆还是浅克隆。

@Test
public void test2() throws CloneNotSupportedException {ClassRoom c1 = new ClassRoom(1, "三年二班");ClassRoom c2 = (ClassRoom) c1.clone();System.out.println(c1.name);System.out.println(c2.name);System.out.println(c1.name == c2.name);
}

执行结果

三年二班
三年二班
true

从输出结果可以看出,c1对象和c2对象的 name 引用属性指向同一字符串对象。也就是说,此时 clone() 方法是浅克隆。


浅克隆改深克隆

在实际应用开发中,浅克隆肯定不能满足所有业务场景。部分情况下,需要将浅克隆优化为深克隆,具体实现方法也很简单:实现 Cloneable 接口,重写 clone() 方法,在 clone() 方法中手动克隆引用属性。下面我们具体看代码:

class Teacher implements Cloneable {int age;String name;public Teacher(int age, String name) {this.age = age;this.name = name;}@Overrideprotected Object clone() throws CloneNotSupportedException {return super.clone();}
}class SchoolRoom implements Cloneable {int num;Teacher teacher;public SchoolRoom(int num, Teacher teacher) {this.num = num;this.teacher = teacher;}@Overrideprotected Object clone() throws CloneNotSupportedException {SchoolRoom result = (SchoolRoom) super.clone();result.teacher = (Teacher) this.teacher.clone();return result;}
}@Test
public void test3() throws CloneNotSupportedException {Teacher teacher = new Teacher(28, "张三");SchoolRoom schoolRoom = new SchoolRoom(40, teacher);SchoolRoom schoolRoom2 = (SchoolRoom) schoolRoom.clone();System.out.println(schoolRoom == schoolRoom2);System.out.println(schoolRoom.teacher == schoolRoom2.teacher);
}

执行结果

false
false

从执行结果可以看出,此时对于 SchoolRoom 类,无论是常量属性还是引用类型属性,clone() 方法前后都不相同。也就是说克隆前后,引用属性指向不同的对象,clone() 方法为深克隆。


深入探讨深克隆

前面我们提到,深克隆是指除基础类型外,引用类型克隆前后指向不同对象。那么克隆对象引用属性的引用属性需要指向不同对象吗。下面我们看代码:

@Test
public void test4() throws CloneNotSupportedException {Teacher teacher = new Teacher(28, "张三");SchoolRoom schoolRoom = new SchoolRoom(40, teacher);SchoolRoom schoolRoom2 = (SchoolRoom) schoolRoom.clone();System.out.println(schoolRoom.teacher.name == schoolRoom2.teacher.name);
}

执行结果

true

从执行结果可以看出,此时虽然 SchoolRoom 的 teacher 引用属性克隆前后指向对象不同,但 teacher 对象的 name 引用属性还是指向同一字符串。用抽象图表示如下:

从上图也就可以看出,实际上此时克隆还不是完全意义上的深克隆,因为 SchoolRoom 对象 teacher 引用的 name 属性克隆前后还指向相同对象。我们可以称这种克隆为 不彻底的深克隆

想要做到完完整整的深克隆,必须保证所有引用属性克隆后都会创建新对象,并且这个过程需要无限向下递归,直到只剩下常量属性。想要实现这种程度的深克隆几乎是不可能的,一旦代码中引入 SDK 包中的类,该类没有重写 clone() 方法,就无法实现。


Object clone() 方法

最后,我们再看看 Object 类的 clone() 方法,具体源码如下:

protected native Object clone() throws CloneNotSupportedException;

从源码可以看出,该方法是 protected 修饰的 native 方法。下面我们看一段代码:

class A{}@Test
public void test5() throws CloneNotSupportedException {A a = new A();A a2 = (A)a.clone(); // mark
}

这段代码我标记的那一行会报编译错误:

'clone()' has protected access in 'java.lang.Object'

具体原因是由于:虽然 Object 类是所有类的父类,并且 clone() 方法是 protected 修饰的方法。当子类和父类不在同一个包时,子类只能访问自身从父类继承而来的受保护的成员,而不能访问父类实例本身受保护的成员。

在上述案例中,我们把代码改成如下所示就不会报错了:

class A implements Cloneable{@Overrideprotected Object clone() throws CloneNotSupportedException {return super.clone();}
}@Test
public void test5() throws CloneNotSupportedException {A a = new A();A a2 = (A)a.clone();
}

关于其中的原理后面我们整理java权限相关知识时再细说。


参考:
https://blog.csdn.net/zhangjg_blog/article/details/18369201
https://blog.csdn.net/qq_37113604/article/details/81168224
https://blog.csdn.net/qq_38962004/article/details/79720416

java clone() 方法详解及深克隆与浅克隆相关推荐

  1. java函数方法详解(简单易懂)

    方法(函数) 函数的组成是: 访问修饰符 返回值 函数名(形式参数) {函数内容; } 更多java函数方法详解视频课程学习地址:https://ke.qq.com/course/149432  有技 ...

  2. java equals方法详解

    序言:准备总结一些java基础的知识方便以后查阅,从equals入手 目录: 等于(==)详解 equals方法详解 一.等于(==)详解 先明确一点:"==" 其实是存储地址的比 ...

  3. JAVA toString方法详解

    JAVA toString方法 在Java中,我们经常会编写许多自定义类.在使用时,我们如何打印出这些类中实例变量? class value {private int s;public void se ...

  4. JAVA本地方法详解,什么是JAVA本地方法?

    https://blog.csdn.net/wi__wi/article/details/51085907 前言: JAVA中有两种方法:JAVA方法和本地方法 JAVA方法是由JAVA编写的,编译成 ...

  5. JAVA Calendar方法详解

    究竟什么是一个 Calendar 呢?中文的翻译就是日历,那我们立刻可以想到我们生活中有阳(公)历.阴(农)历之分.它们的区别在哪呢? 比如有:     月份的定义 - 阳`(公)历 一年12 个月, ...

  6. Java main 方法详解

    1.main方法说起 编译完我们的java文件后,需要有个一含有main方法的类,java 命令将指示操作系统启动一个jvm进程 这个jvm进程启动后,寻找那个main地方开始执行程序 java [J ...

  7. Java回调方法详解

    回调在维基百科中定义为: 在计算机程序设计中,回调函数,是指通过函数参数传递到其他代码的,某一块可执行代码的引用. 其目的是允许底层代码调用在高层定义的子程序. 举个例子可能更明白一些:以Androi ...

  8. [译] Java 桥接方法详解

    原文地址:Java bridge methods explained 原文作者:STAS 译文出自:掘金翻译计划 本文永久链接:github.com/xitu/gold-m- 译者:kezhenxu9 ...

  9. Java equals 方法详解

    equals()方法: 是一个方法,而非运算符 只能适用于引用数据类型 Object类中equls()的定义: public boolean equals(Object obj) {return (t ...

最新文章

  1. CV算法复现(分类算法6/6):MobileNet(2017年V1,2018年V2,2019年V3,谷歌)
  2. 安装php出现php-cgi error 1
  3. mysql 超时异常_java.sql.SQLException:超出锁定等待超时;尝试在MYSQL中重启事务异常...
  4. Leetcode-一篇帖子就够啦
  5. AB1601编译优化参数引发的问题
  6. Dubbo + Zookeeper入门初探
  7. 通过引入switch表达式来增强Java switch语句
  8. 最近的日子,很惬意!
  9. linux启用dcb步骤,Linux DCB体系——简短概述
  10. Keras下使用多GPU训练模型
  11. 外贸SOHO具备的素质
  12. 安卓开发(简单打开前置摄像头并显示)
  13. Learn Git Branching学习笔记 Git常用命令
  14. 一张专家推荐的最健康的作息时间表
  15. excel按条件选择工作表_在Excel工作表中选择“实际使用范围”
  16. anmate.css怎么用,animate.css使用方法是什么
  17. 使用LaTeX写数学公式
  18. CSS基础之 背景属性设置
  19. C语言里的和*的简单作用理解
  20. 安装torch0.4.1的神坑

热门文章

  1. 如何使用freeMarker生成doc、docx文档
  2. B端体验度量衡-体验度量衡指标以及实施篇
  3. F3飞控下载程序方式
  4. iOS App 设置启动图(LaunchImage)
  5. 解决微信小程序请求后端接口碰到合法域名的问题 http-405j及java接口和数据接口的概念区分
  6. 人工智能-逻辑回归、分类评估方法、ROC曲线、类别不平衡
  7. 开源服务器监控工具——zabbix(二)
  8. 清新旅拍电影质感妆容人像修饰调色Lightroom预设
  9. 废旧笔记本改造安装黑群晖打造私人NAS超级详细图文教程
  10. 装饰者设计模式(java版本)