java clone() 方法详解及深克隆与浅克隆

java clone() 方法详解及深克隆与浅克隆

概述

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

从执行结果可以看出,即使是两个不同的引用,指向相同对象时,他们也是相同的,并且值都是指向对象的地址。简单绘图表示的话,如下所示:
图1
有了上面的基础,我们再来看 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() 方法创建了一个新的对象:
图2


深克隆与浅克隆

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

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

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

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

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

下面我通过抽象图的形式描述深克隆和浅克隆的区别:
图3
通过上图可以明显看出,深克隆时 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 引用属性还是指向同一字符串。用抽象图表示如下:
图4
从上图也就可以看出,实际上此时克隆还不是完全意义上的深克隆,因为 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权限相关知识时再细说。


参考:

详解Java中的clone方法 -- 原型模式_昨夜星辰_zhangjg的博客-CSDN博客

Cloneable接口的作用与深入理解深度克隆与浅度克隆_漫步夕阳下的博客-CSDN博客

由Object.clone()引出的protected权限问题_BigFishAndBegonia的博客-CSDN博客

  • 1
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值