没看这篇文章之前,我以为真的懂深克隆和浅克隆

2023-10-10 18:59

本文主要是介绍没看这篇文章之前,我以为真的懂深克隆和浅克隆,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

面试题:深克隆和浅克隆的实现方式

面试官考察点

考察目的:深克隆和浅克隆,考察的是Java基础知识的理解。

考察人群:2到5年开发经验。

背景知识详解

先了解下浅克隆和深克隆的定义:

  1. 浅克隆:被复制对象的所有变量都含有与原来的对象相同的值,而所有的对其他对象的引用仍然指向原来的对象。

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

如何实现克隆

我么先不管深克隆、还是浅克隆。首先,要先了解如何实现克隆,实现克隆需要满足以下三个步骤

  1. 对象的类实现Cloneable接口;

  2. 覆盖Object类的clone()方法(覆盖clone()方法,访问修饰符设为public,默认是protected,但是如果所有类都在同一个包下protected是可以访问的);

  3. 在clone()方法中调用super.clone();

实现一个克隆

先定义一个score类,表示分数信息。

public class Score {private String category;private double fraction;public Score() {}public Score(String category, double fraction) {this.category = category;this.fraction = fraction;}//getter/setter省略@Overridepublic String toString() {return "Score{" +"category='" + category + '\'' +", fraction=" + fraction +'}';}
}

定义一个Person,其中包含Score属性,来表示这个人的考试分数。

需要注意,Person类是实现了Cloneable接口的,并且重写了clone()这个方法。

public class Person implements Cloneable{private String name;private int age;private List<Score> score;public Person() {}@Overrideprotected Object clone() throws CloneNotSupportedException {return super.clone();}
}

克隆代码测试,代码逻辑不复杂,就是初始化一个对象mic,然后基于mic使用clone方法克隆出一个对象dylan

接着通过修改被克隆对象mic的成员属性,打印出这两个对象的状态信息。

public class CloneMain {public static void main(String[] args) throws CloneNotSupportedException {Person mic=new Person();Score s1=new Score();s1.setCategory("语文");s1.setFraction(90);Score s2=new Score();s2.setCategory("数学");s2.setFraction(100);mic.setAge(18);mic.setName("Mic");mic.setScore(Arrays.asList(s1,s2));System.out.println("person对象初始化状态:"+mic);Person dylan=(Person)mic.clone(); //克隆一个对象System.out.println("打印克隆对象:dylan:"+dylan);mic.setAge(20);mic.getScore().get(0).setFraction(70); //修改mic语文分数为70System.out.println("打印mic:"+mic);System.out.println("打印dylan:"+dylan);}
}

执行结果如下:

person对象初始化状态:Person{name='Mic', age=18, score=[Score{category='语文', fraction=90.0}, Score{category='数学', fraction=100.0}]}
打印克隆对象:dylan:Person{name='Mic', age=18, score=[Score{category='语文', fraction=90.0}, Score{category='数学', fraction=100.0}]}
打印mic:Person{name='Mic', age=20, score=[Score{category='语文', fraction=70.0}, Score{category='数学', fraction=100.0}]}
打印dylan:Person{name='Mic', age=18, score=[Score{category='语文', fraction=70.0}, Score{category='数学', fraction=100.0}]}

从结果中可以发现:

  1. 修改mic对象本身的普通属性age,发现该属性的修改只影响到mic对象本身的实例。

  2. 当修改mic对象的语文成绩时,dylan对象的语文成绩也发生了变化。

为什么会导致这个现象?回过头看一下浅克隆的定义:

浅克隆:创建一个新对象,新对象的属性和原来对象完全相同,对于非基本类型属性,仍指向原有属性所指向的对象的内存地址

需要特别强调非基本类型,对于非基本类型,传递的是值,所以新的dylan对象会对该属性创建一个副本。同样,对于final修饰的属性,由于它的不可变性,在浅克隆时,也会在内存中创建副本。

如图所示,dylan对象从mic对象克隆过来后,dylan对象的内存地址指向的是同一个。因此当mic这个对象中的属性发生变化时,dylan对象的属性也会发生变化。

7258a119f3510ac8107a124d197d404c.png

clone方法的源码分析

经过上述案例演示可以发现,如果对象实现Cloneable并重写clone方法不进行任何操作时,调用clone是进行的浅克隆,那clone方法是如何实现的呢?它默认情况下做了什么?

clone方法是Object中默认提供的,它的源码定义如下

protected native Object clone() throws CloneNotSupportedException;

从源码中我们可以看到几个关键点:

1.clone方法是native方法,native方法的效率远高于非native方法,因此如果我们需要拷贝一个对象,建议使用clone,而不是new。

2.该方法被protected修饰。这就意味着想要使用,则必须重写该方法,并且设置成public。

3.返回值是一个Object对象,因此通过clone方法克隆一个对象,需要强制转换。

4.如果在没有实现Cloneable接口的实例上调用Object的clone()方法,则会导致抛出CloneNotSupporteddException;

再来看一下Object.clone方法上的注释,注释的内容有点长。

/*** Creates and returns a copy of this object.  The precise meaning* of "copy" may depend on the class of the object. The general* intent is that, for any object {@code x}, the expression:* <blockquote>* <pre>* x.clone() != x</pre></blockquote>* will be true, and that the expression:* <blockquote>* <pre>* x.clone().getClass() == x.getClass()</pre></blockquote>* will be {@code true}, but these are not absolute requirements.* While it is typically the case that:* <blockquote>* <pre>* x.clone().equals(x)</pre></blockquote>* will be {@code true}, this is not an absolute requirement.* <p>* By convention, the returned object should be obtained by calling* {@code super.clone}.  If a class and all of its superclasses (except* {@code Object}) obey this convention, it will be the case that* {@code x.clone().getClass() == x.getClass()}.* <p>* By convention, the object returned by this method should be independent* of this object (which is being cloned).  To achieve this independence,* it may be necessary to modify one or more fields of the object returned* by {@code super.clone} before returning it.  Typically, this means* copying any mutable objects that comprise the internal "deep structure"* of the object being cloned and replacing the references to these* objects with references to the copies.  If a class contains only* primitive fields or references to immutable objects, then it is usually* the case that no fields in the object returned by {@code super.clone}* need to be modified.* <p>* The method {@code clone} for class {@code Object} performs a* specific cloning operation. First, if the class of this object does* not implement the interface {@code Cloneable}, then a* {@code CloneNotSupportedException} is thrown. Note that all arrays* are considered to implement the interface {@code Cloneable} and that* the return type of the {@code clone} method of an array type {@code T[]}* is {@code T[]} where T is any reference or primitive type.* Otherwise, this method creates a new instance of the class of this* object and initializes all its fields with exactly the contents of* the corresponding fields of this object, as if by assignment; the* contents of the fields are not themselves cloned. Thus, this method* performs a "shallow copy" of this object, not a "deep copy" operation.* <p>* The class {@code Object} does not itself implement the interface* {@code Cloneable}, so calling the {@code clone} method on an object* whose class is {@code Object} will result in throwing an* exception at run time.** @return     a clone of this instance.* @throws  CloneNotSupportedException  if the object's class does not*               support the {@code Cloneable} interface. Subclasses*               that override the {@code clone} method can also*               throw this exception to indicate that an instance cannot*               be cloned.* @see java.lang.Cloneable*/protected native Object clone() throws CloneNotSupportedException;

上述方法中的注释描述中,对于clone方法关于复制描述,提出了三个规则,也就是说,”复制“的确切定义取决于对象本身,它可以满足以下任意一条规则:

  • 对于所有对象,x.clone () !=x 应当返回 true,因为克隆对象与原对象不是同一个对象。

  • 对于所有对象,x.clone ().getClass () == x.getClass () 应当返回 true,因为克隆对象与原对象的类型是一样的。

  • 对于所有对象,x.clone ().equals (x) 应当返回 true,因为使用 equals 比较时,它们的值都是相同的。

因此,从clone方法的源码中可以得到一个结论,clone方法是深克隆还是浅克隆,取决于实现克隆方法对象的本身实现。

深克隆

理解了浅克隆,我们就不难猜测到,所谓深克隆的本质,应该是如下图所示。

e9b925303d311d390f68447371ce3b95.png

dylan这个对象实例从mic对象克隆之后,应该要分配一块新的内存地址,从而实现在内存地址上的隔离。

深拷贝实现的是对所有可变(没有被final修饰的引用变量)引用类型的成员变量都开辟独立的内存空间,使得拷贝对象和被拷贝对象之间彼此独立,因此一般深拷贝对于浅拷贝来说是比较耗费时间和内存开销的。

深克隆实现

修改Person类中的clone()方法,代码如下。

@Override
protected Object clone() throws CloneNotSupportedException {Person p=(Person)super.clone(); //可以直接使用clone方法克隆,因为String类型中的属性是final修饰,而int是基本类型,都会创建副本if(this.score!=null&&this.score.size()>0){ //如果score不为空时,才做深度克隆//由于`score`是引用类型,所以需要重新分配内存空间List<Score> ls=new ArrayList<>();this.score.stream().forEach(score->{Score s=new Score();s.setFraction(score.getFraction());s.setCategory(score.getCategory());ls.add(s);});p.setScore(ls);}return p;
}

再次执行,运行结果如下

person对象初始化状态:Person{name='Mic', age=18, score=[Score{category='语文', fraction=90.0}, Score{category='数学', fraction=100.0}]}
打印克隆对象:dylan:Person{name='Mic', age=18, score=[Score{category='语文', fraction=90.0}, Score{category='数学', fraction=100.0}]}
打印mic:Person{name='Mic', age=20, score=[Score{category='语文', fraction=70.0}, Score{category='数学', fraction=100.0}]}
打印dylan:Person{name='Mic', age=18, score=[Score{category='语文', fraction=90.0}, Score{category='数学', fraction=100.0}]}Process finished with exit code 0

从结果可以看到,这两个对象之间并没有相互影响,因为我们在clone方法中,对于Person这个类的成员属性Score使用new创建了一个新的对象,这样就使得两个对象分别指向不同的内存地址。

创建一个新对象,属性中引用的其他对象也会被克隆,不再指向原有对象地址。总之深浅克隆都会在堆中新分配一块区域,区别在于对象属性引用的对象是否需要进行克隆(递归性的)

深克隆的其他实现方式

深克隆的实现方式很多,总的来说有以下几种:

  • 所有对象都实现克隆方法。

  • 通过构造方法实现深克隆。

  • 使用 JDK 自带的字节流。

  • 使用第三方工具实现,比如:Apache Commons Lang。

  • 使用 JSON 工具类实现,比如:Gson,FastJSON 等等。

其实,深克隆既然是在内存中创建新的对象,那么任何能够创建新实例对象的方式都能完成这个动作,因此不局限于这些方法。

所有对象都实现克隆方法

由于浅克隆本质上是因为引用对象指向同一块内存地址,如果每个对象都实现克隆方法,意味着每个对象的最基本单位是基本数据类型或者封装类型,而这些类型在克隆时会创建副本,从而避免了指向同一块内存地址的问题。

修改代码如下。

public class Person implements Cloneable {private String name;private int age;private List<Score> score;public Person() {}@Overrideprotected Object clone() throws CloneNotSupportedException {Person p=(Person)super.clone();if(this.score!=null&&this.score.size()>0){ //如果score不为空时,才做深度克隆//由于`score`是引用类型,所以需要重新分配内存空间List<Score> ls=new ArrayList<>();this.score.stream().forEach(score->{try {ls.add((Score)score.clone()); //这里用了克隆方法} catch (CloneNotSupportedException e) {e.printStackTrace();}});p.setScore(ls);}return p;}
}

修改Score对象

public class Score implements Cloneable {private String category;private double fraction;public Score() {}public Score(String category, double fraction) {this.category = category;this.fraction = fraction;}@Overrideprotected Object clone() throws CloneNotSupportedException {return super.clone();}
}
Person dylan=(Person)mic.clone(); //克隆一个对象

运行结果如下

person对象初始化状态:Person{name='Mic', age=18, score=[Score{category='语文', fraction=90.0}, Score{category='数学', fraction=100.0}]}
打印克隆对象:dylan:Person{name='Mic', age=18, score=[Score{category='语文', fraction=90.0}, Score{category='数学', fraction=100.0}]}
打印mic:Person{name='Mic', age=20, score=[Score{category='语文', fraction=70.0}, Score{category='数学', fraction=100.0}]}
打印dylan:Person{name='Mic', age=18, score=[Score{category='语文', fraction=90.0}, Score{category='数学', fraction=100.0}]}

通过构造方法实现深克隆。

构造方法实现深克隆,其实是我们经常使用的方法,就是使用new关键字来实例化一个新的对象,然后通过构造参数传值来实现数据拷贝。

public class Person implements Cloneable {private String name;private int age;private List<Score> score;public Person() {}public Person(String name, int age, List<Score> score) {this.name = name;this.age = age;this.score = score;}
}

克隆的时候,我们这么做

Person dylan=new Person(mic.getName(),mic.getAge(),mic.getScore()); //克隆一个对象

基于ObjectStream实现深克隆

在Java中,对象流也可以实现深克隆,大家可能对对象流这个名词有点陌生,它的定义如下:

  • ObjectOutputStream, 对象输出流,把一个对象转换为二进制格式数据

  • ObjectInputStream,对象输入流,把一个二进制数据转换为对象。

这两个对象,在Java中通常用来实现对象的序列化。

创建一个工具类,使用ObjectStream来实现对象的克隆,代码实现逻辑不难:

  1. 使用ObjectOutputStream,把一个对象转换为数据流存储到对象ByteArrayOutputStream中。

  2. 再从内存中读取该数据流,使用ObjectInputStream,把该数据流转换为目标对象。

public class ObjectStreamClone {public static <T extends Serializable> T clone(T t){T cloneObj = null;try {// bo,存储对象输出流,写入到内存ByteArrayOutputStream bo = new ByteArrayOutputStream();//对象输出流,把对象转换为数据流ObjectOutputStream oos = new ObjectOutputStream(bo);oos.writeObject(t);oos.close();// 分配内存,写入原始对象,生成新对象ByteArrayInputStream bi = new ByteArrayInputStream(bo.toByteArray());ObjectInputStream oi = new ObjectInputStream(bi);// 返回生成的新对象cloneObj = (T) oi.readObject();oi.close();} catch (Exception e) {e.printStackTrace();}return cloneObj;}}

Person对象和Score对象均需要实现Serializable接口,

public class Person implements Serializable {
}
public class Score implements Serializable {}

修改测试类的克隆方法.

Person dylan=(Person)ObjectStreamClone.clone(mic); //克隆一个对象

运行结果如下:

person对象初始化状态:Person{name='Mic', age=18, score=[Score{category='语文', fraction=90.0}, Score{category='数学', fraction=100.0}]}
打印克隆对象:dylan:Person{name='Mic', age=18, score=[Score{category='语文', fraction=90.0}, Score{category='数学', fraction=100.0}]}
打印mic:Person{name='Mic', age=20, score=[Score{category='语文', fraction=70.0}, Score{category='数学', fraction=100.0}]}
打印dylan:Person{name='Mic', age=18, score=[Score{category='语文', fraction=90.0}, Score{category='数学', fraction=100.0}]}

通过对象流能够实现深克隆,其根本原因还是在于对象的序列化之后,已经脱离了JVM内存对象的范畴,毕竟一个对象序列化之后,是可以通过文件、或者网络跨JVM传输的,因此对象在反序列化时,必然需要基于该数据流重新反射生成新的对象。

问题解答

问题:深克隆和浅克隆的实现方式

回答:

  1. 浅克隆是指被复制对象中属于引用类型的成员变量的内存地址和被克隆对象的内存地址相同,也就是克隆对象只实现了对被克隆对象基本类型的副本克隆。

    浅克隆的实现方式,可以实现Cloneable接口,并重写clone方法,即可完成浅克隆。

    浅克隆的好处是,避免了引用对象的内存分配和回收,提高对象的复制效率。

  2. 深克隆是指实现对于基本类型和引用类型的完整克隆,克隆对象和被克隆对象中的引用对象的内存地址完全隔离。

    深克隆的实现方式:

  • 基于Cloneable接口重写clone方法,但是我们需要在clone方法中,针对应用类型的成员变量,使用new关键字分配独立的内存空间。

  • 基于Java中对象流的方式实现

  • 基于构造方法实现深度克隆

  • 被克隆的对象中所有涉及到引用类型变量的对象,全部实现克隆方法,并且在被克隆对象的clone方法中,需要调用所有成员对象的clone方法实现对象克隆

问题总结

深克隆的本质,其实是保证被克隆对象中所有应用对象以及引用所嵌套的引用对象,全部分配一块独立的内存空间,避免克隆对象和被克隆对象指向同一块内存地址,造成数据错误等问题。

所以,深克隆,表示对象拷贝的深度,因为在Java中对象的嵌套是非常常见的。理解了这个知识点,才能避免在开发过程中遇到一些奇奇怪怪的问题。

这篇关于没看这篇文章之前,我以为真的懂深克隆和浅克隆的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/182427

相关文章

为什么现在很多人愿意选择做债务重组?债重组真的就这么好吗?

债务重组,起初作为面向优质企业客户的定制化大额融资策略,以其高效周期著称,一个月便显成效。然而,随着时代的车轮滚滚向前,它已悄然转变为负债累累、深陷网贷泥潭者的救赎之道。在此路径下,个人可先借助专业机构暂代月供,经一段时间养护征信之后,转向银行获取低成本贷款,用以替换高昂网贷,实现利息减负与成本优化的双重目标。 尽管债务重组的代价不菲,远超传统贷款成本,但其吸引力依旧强劲,背后逻辑深刻。其一

day45-测试平台搭建之前端vue学习-基础4

目录 一、生命周期         1.1.概念         1.2.常用的生命周期钩子         1.3.关于销毁Vue实例         1.4.原理​编辑         1.5.代码 二、非单文件组件         2.1.组件         2.2.使用组件的三大步骤         2.3.注意点         2.4.关于VueComponen

ArcGIS Pro 克隆clone python环境报错问题处理方法

ArcGIS Pro 克隆clone python环境报错问题处理方法 (一)安装arcpro和深度学习安装包 首先安装arcgis pro桌面版和深度学习安装包后 然后克隆默认 Python 环境 arcgispro-py3 接下来,安装以下 Python 软件包:Tensorflow、fast.ai、Keras、Pytorch、Scikit-image、Pillow 和 Libtiff。 切

写给大数据开发:你真的“慢“了吗?揭秘技术与职场的平衡艺术

你是否曾经在深夜里,面对着一个棘手的数据处理问题,感到无比沮丧?或者在一次重要的项目汇报中,突然语塞,无法清晰地表达你的技术方案?作为一名大数据开发者,这些场景可能再熟悉不过。但别担心,因为你并不孤单。让我们一起探讨如何在这个瞬息万变的行业中,既磨练技术利刃,又培养职场软实力。 目录 技术与时间的赛跑1. 长远视角的重要性2. 复利效应在技能学习中的应用 跨界思维:数据结构教我们的职场智

【数据结构】你真的学会了二叉树了吗,来做一做二叉树的算法题及选择题

文章目录 1. 二叉树算法题1.1 单值二叉树1.2 相同的树1.3 另一棵树的子树1.4 二叉树的遍历1.5 二叉树的构建及遍历 2. 二叉树选择题3. 结语 1. 二叉树算法题 1.1 单值二叉树 https://leetcode.cn/problems/univalued-binary-tree/description/ 1.2 相同的树 https://leet

一个瑞典游戏工作室决定离开索尼,之前和之后都发生了什么?

我们在前两篇中探究了国家政策、硬件基础与黑客文化如何让瑞典成为了游戏热土,而它充满地域特色的开发者社区与教育体系的构建,又是如何聚拢了游戏人才,让体系持续生长扩张。 除了大学、科技园和开发者之家外,我们此行从斯德哥尔摩到舍夫德到马尔默,还采访了三家知名工作室的创始人。它们一家产出如今罕见的双人合作游戏,还有一位特立独行的作者型开发者屡屡占据头条;一家贡献了现象级网红作品,当前在朝“正经向”大

193篇文章暴揍Flink,这个合集你需要关注一下

点击上方蓝色字体,选择“设为星标” 回复”资源“获取更多惊喜 前一段时间我写了一篇:《我们在学习Flink的时候,到底在学习什么?》。 基本上把大多数情况下Flink需要学习的点都照顾到了。 然后重点来了,我整理了一个合集放在了CSDN论坛,根据Flink版本发布过程和知识点,收录了网络上写的比较好的文章,基本覆盖了近100%的Flink的知识点。点击文末的【阅读原文】可以跳转,你有必要收藏一

数字时代,寻找新的生意增长点之前要做什么准备?

要做好最基础也最繁复的数据管理。 在竞争日益激烈的快消市场中,企业面临前所未有的挑战与压力。在这种高压环境下,数字化转型不再仅仅是选择,而是企业探索新的业务增长点、保持竞争优势的关键战略。然而,随着企业数字化进程的加速推进,业务系统持续生成的多样化与复杂化数据使得传统数据分析手段难以应对。因各系统间业财口径的不一致和数据维度的差异,企业在数据整合与分析过程中经常遭遇瓶颈,难以获得准确且具有前瞻性

汇总(二):之前

1.git命令行 2.Mysql基本sql语句 3.SpringBoot相关注解:(1)RestController (2)RequestMapping (3)Autowired (4)crossOrithVariabgin (5)PutMapping (6)PostMapping (7)RequestBody (8)RequestParam (9)PathVariable (10)Servi

4所适合职场人读的海外在线硕士院校,真的靠谱!

不用参加硕士研究生统考、不用出国、线上上网课就能读全球排名前列院校的硕士研究生。 这让很多想自我提升,但是因为工作繁忙没时间准备国内硕士联考、也没时间出国留学,以及一些毕业多年,应试能力下降、多次联考不过的职场人士有些蠢蠢欲动。 但是这个领域实在过于陌生,很多同学不了解、也没有靠谱的了解渠道,看到各种世界排名靠前院校的海外硕士的招生信息,心动过后也只能继续观望。 不出国门、免考试,就能获取海