面试官:可以聊聊Java的浅拷贝和深拷贝吗?

我是「猿码天地」,一个热爱技术、热爱编程的IT猿。技术是开源的,知识是共享的!  

写作是对自己学习的总结和记录,如果您对Java、分布式、微服务、中间件、Spring Boot、Spring Cloud等技术感兴趣,可以关注我的动态,我们一起学习,一起成长! 

用知识改变命运,让家人过上更好的生活,互联网人一家亲!

微信搜索「猿码天地」,回复「电子书」白嫖1000本Java开发精华电子书,回复「BAT面试」获取最新国内一线大厂Java面试题!我的微信:zhangbowen125 有任何问题欢迎私聊咨询!

先来一波粉丝福利,猿人整理的Java知识学堂脑图Java超神之路脑图,非常值得学习!

猿码天地-Java知识学堂脑图 (文件密码:请关注公众号【猿码天地】,回复关键字‘活到老学到老’获取)
猿码天地-Java超神之路脑图 (文件密码:请关注公众号【猿码天地】,回复关键字‘活到老学到老’获取)

1. 写在前面

关于Java的深拷贝和浅拷贝,简单来说就是创建一个和已知对象一模一样的对象。可能日常编码过程中用的不多,但是这是一个面试经常会问的问题,而且了解深拷贝和浅拷贝的原理,对于Java中的所谓值传递或者引用传递将会有更深的理解。

2. 创建对象的5种方式

  • 通过 new 关键字
这是最常用的一种方式,通过 new 关键字调用类的有参或无参构造方法来创建对象。比如 Object obj = new Object();
  • 通过 Class 类的 newInstance() 方法
这种默认是调用类的无参构造方法创建对象。比如 Person p2 = (Person) Class.forName("com.ys.test.Person").newInstance();
  • 通过 Constructor 类的 newInstance 方法
这和第二种方法类时,都是通过反射来实现。通过 java.lang.relect.Constructor 类的 newInstance() 方法指定某个构造器来创建对象。

Person p3 = (Person) Person.class.getConstructors()[0].newInstance();

实际上第二种方法利用 Class 的 newInstance() 方法创建对象,其内部调用还是 Constructor 的 newInstance() 方法。
  • 利用 Clone 方法
Clone 是 Object 类中的一个方法,通过 对象A.clone() 方法会创建一个内容和对象 A 一模一样的对象 B,clone 克隆,顾名思义就是创建一个一模一样的对象出来。

Person p4 = (Person) p3.clone();
  • 反序列化
序列化是把堆内存中的 Java 对象数据,通过某种方式把对象存储到磁盘文件中或者传递给其他网络节点(在网络上传输)。而反序列化则是把磁盘文件中的对象数据或者把网络节点上的对象数据,恢复成Java对象模型的过程。

3. Clone方法

本篇文章我们讲解的是 Java 的深拷贝和浅拷贝,其实现方式正是通过调用 Object 类的 clone() 方法来完成。在 Object.class 类中,源码为:

protected native Object clone() throws CloneNotSupportedException;

这是一个用 native 关键字修饰的方法,不理解也没关系,只需要知道用 native 修饰的方法就是告诉操作系统,这个方法我不实现了,让操作系统去实现。具体怎么实现我们不需要了解,只需要知道 clone方法的作用就是复制对象,产生一个新的对象。那么这个新的对象和原对象是什么关系呢?

4. 基本类型和引用类型

在 Java 中数据类型可以分为两大类:基本类型引用类型

基本类型也称为值类型,分别是字符类型 char,布尔类型 boolean以及数值类型 byte、short、int、long、float、double

引用类型则包括类、接口、数组、枚举等

Java 将内存空间分为堆和栈。基本类型直接在栈中存储数值,而引用类型是将引用放在栈中,实际存储的值是放在堆中,通过栈中的引用指向堆中存放的数据。

面试官:可以聊聊Java的浅拷贝和深拷贝吗?_第1张图片

上图定义的 a 和 b 都是基本类型,其值是直接存放在栈中的;而 c 和 d 是 String 声明的,这是一个引用类型,引用地址是存放在 栈中,然后指向堆的内存空间。

下面 d = c;这条语句表示将 c 的引用赋值给 d,那么 c 和 d 将指向同一块堆内存空间。

5. 浅拷贝

在讲解浅拷贝之前,我们用一段代码来实现它。

/**
 * 

java-study

*

浅拷贝

* @author : zhang.bw * @date : 2021-04-29 14:01 **/
public class Person implements Cloneable{ public String pname; public int page; public Address address; public Person() {} public Person(String pname,int page){ this.pname = pname; this.page = page; this.address = new Address(); } @Override protected Object clone() throws CloneNotSupportedException { return super.clone(); } public void setAddress(String provices,String city ){ address.setAddress(provices, city); } public void display(String name){ System.out.println(name+":"+"pname=" + pname + ", page=" + page +","+ address); } public String getPname() { return pname; } public void setPname(String pname) { this.pname = pname; } public int getPage() { return page; } public void setPage(int page) { this.page = page; } }
/**
 * 

java-study

*

浅拷贝

* @author : zhang.bw * @date : 2021-04-29 14:03 **/
public class Address { private String provices; private String city; public void setAddress(String provices,String city){ this.provices = provices; this.city = city; } @Override public String toString() { return "Address [provices=" + provices + ", city=" + city + "]"; } }

上面的代码是一个我们要进行赋值的原始类 Person。下面我们产生一个 Person 对象,并调用其 clone 方法复制一个新的对象。

注意:调用对象的 clone 方法,必须要让类实现 Cloneable 接口,并且覆写 clone 方法。

/**
 * 

java-study

*

浅拷贝测试

* @author : zhang.bw * @date : 2021-04-29 14:08 **/
public class TestPerson { public static void main(String[] args) throws Exception{ Person p1 = new Person("zhangsan",21); p1.setAddress("湖北省", "武汉市"); Person p2 = (Person) p1.clone(); System.out.println("p1:"+p1); System.out.println("p1.getPname:"+p1.getPname().hashCode()); System.out.println("p2:"+p2); System.out.println("p2.getPname:"+p2.getPname().hashCode()); p1.display("p1"); p2.display("p2"); p2.setAddress("湖北省", "荆州市"); System.out.println("将复制之后的对象地址修改:"); p1.display("p1"); p2.display("p2"); } }

面试官:可以聊聊Java的浅拷贝和深拷贝吗?_第2张图片

首先看原始类 Person 实现 Cloneable 接口,并且覆写 clone 方法,它还有三个属性,一个引用类型 String定义的 pname,一个基本类型 int定义的 page,还有一个引用类型 Address ,这是一个自定义类,这个类也包含两个属性 pprovices 和 city 。

接着看测试内容,首先我们创建一个Person 类的对象 p1,其pname 为zhangsan,page为21,地址类 Address 两个属性为 湖北省和武汉市。接着我们调用 clone() 方法复制另一个对象 p2,接着打印这两个对象的内容。

从第 1 行和第 3 行打印结果:

p1:com.bowen.demo.demo008.method1.Person@5dfe23e8  
p2:com.bowen.demo.demo008.method1.Person@583fb274  

可以看出这是两个不同的对象。

从第 5 行和第 6 行打印的对象内容看,原对象 p1 和克隆出来的对象 p2 内容完全相同。

代码中我们只是更改了克隆对象 p2 的属性 Address 为湖北省荆州市(原对象 p1 是湖北省武汉市) ,但是从第 7 行和第 8 行打印结果来看,原对象 p1 和克隆对象 p2 的 Address 属性都被修改了。

也就是说对象 Person 的属性 Address,经过 clone 之后,其实只是复制了其引用,他们指向的还是同一块堆内存空间,当修改其中一个对象的属性 Address,另一个也会跟着变化。

面试官:可以聊聊Java的浅拷贝和深拷贝吗?_第3张图片

浅拷贝:创建一个新对象,然后将当前对象的非静态字段复制到该新对象,如果字段是值类型的,那么对该字段执行复制;如果该字段是引用类型的话,则复制引用但不复制引用的对象。因此,原始对象及其副本引用同一个对象。

6. 深拷贝

弄清楚了浅拷贝,那么深拷贝就很容易理解了。

深拷贝:创建一个新对象,然后将当前对象的非静态字段复制到该新对象,无论该字段是值类型的还是引用类型,都复制独立的一份。当你修改其中一个对象的任何内容时,都不会影响另一个对象的内容。

面试官:可以聊聊Java的浅拷贝和深拷贝吗?_第4张图片

那么该如何实现深拷贝呢?Object 类提供的 clone 是只能实现浅拷贝的。

7. 如何实现深拷贝?

深拷贝的原理我们知道了,就是要让原始对象和克隆之后的对象所具有的引用类型属性不是指向同一块堆内存,这里有两种实现思路。

方法一:让每个引用类型属性内部都重写clone() 方法

既然引用类型不能实现深拷贝,那么我们将每个引用类型都拆分为基本类型,分别进行浅拷贝。比如上面的例子,Person 类有一个引用类型 Address(其实String 也是引用类型,但是String类型有点特殊,后面会详细讲解),我们在 Address 类内部也重写 clone 方法。如下:

Address.class:

/**
 * 

java-study

*

浅拷贝

* @author : zhang.bw * @date : 2021-04-29 14:03 **/
public class Address implements Cloneable{ private String provices; private String city; public void setAddress(String provices,String city){ this.provices = provices; this.city = city; } @Override public String toString() { return "Address [provices=" + provices + ", city=" + city + "]"; } @Override protected Object clone() throws CloneNotSupportedException { return super.clone(); } }

Person.class 的 clone() 方法:

@Override
	protected Object clone() throws CloneNotSupportedException {
		Person p = (Person) super.clone();
		p.address = (Address) address.clone();
		return p;
	}

测试还是和上面一样,我们会发现更改了p2对象的Address属性,p1 对象的 Address 属性并没有变化。

面试官:可以聊聊Java的浅拷贝和深拷贝吗?_第5张图片

但是这种做法有个弊端,这里我们Person 类只有一个 Address 引用类型,而 Address 类没有,所以我们只用重写 Address 类的clone 方法,但是如果 Address 类也存在一个引用类型,那么我们也要重写其clone 方法,这样下去,有多少个引用类型,我们就要重写多少次,如果存在很多引用类型,那么代码量显然会很大,所以这种方法不太合适。

方法二:利用序列化

序列化是将对象写到流中便于传输,而反序列化则是把对象从流中读取出来。这里写到流中的对象则是原始对象的一个拷贝,因为原始对象还存在 JVM 中,所以我们可以利用对象的序列化产生克隆对象,然后通过反序列化获取这个对象。

注意每个需要序列化的类都要实现 Serializable 接口,如果有某个属性不需要序列化,可以将其声明为 transient,即将其排除在克隆属性之外。

//深度拷贝
public Object deepClone() throws Exception{
    // 序列化
    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();
}

因为序列化产生的是两个完全独立的对象,所有无论嵌套多少个引用类型,序列化都是能实现深拷贝的。

源码地址:https://gitee.com/zhangbw666/java-study

粉丝福利

猿码天地-Java知识学堂脑图 (文件密码:请关注公众号【猿码天地】,回复关键字‘活到老学到老’获取)
猿码天地-Java超神之路脑图 (文件密码:请关注公众号【猿码天地】,回复关键字‘活到老学到老’获取)

福利一:月薪3万Java优秀简历模板

简历是对你过往学习和工作的一个总结,一份好的简历容易给人留下深刻印象,更容易被企业发现,创造更多的就业机会。优秀的简历可以把你推销给优秀的企业,看看别人的优秀简历范文,让自己的简历更加优秀吧。

可以在微信搜索「猿码天地」,关注后回复关键字「简历」即可获取!

福利二:全网最全一线大厂Java笔试面试题

面试题涵盖了Spring核心知识、SpringMVC、SpirngBoot、SpringCloud、JVM、数据库、常用消息组件Kafka、RabbitMQ、分布式、微服务组件Eureka、Ribbon、Config、Feign、Nacos、Zookeeper等、SQL优化、常用服务器Tomcat、Nginx、多线程、高并发等等。

可以在微信搜索「猿码天地」,关注后回复关键字「BAT面试」即可获取!

福利三:1000+本Java开发精华电子书

猿人从事Java开发多年,从最初的小白,一步步通过自己不断的学习、摸索,终于走上了码农这条没有回头的路。学习是无止境的,有时零零散散的学习资料难以将我们的知识成体系的串联起来。为此,为了让大家在学习Java的道路上成系统的学习,少走弯路,猿人为大家整理了1000+本Java开发精华电子书,毕竟现在电子书也是挺贵的,哈哈,希望能给大家带来帮助。

电子书涵盖:Java入门、并发编程、底层、常用框架、性能优化、设计模式、工具、计算机网络、操作系统、数据库、数据结构与算法、大数据、架构设计、Linux知识、面试、扩展、管理、容器化技术、云计算、微服务、领域设计等方方面面,猿人还会给持续更新。

获取方式

第一种:微信搜索「猿码天地」或者扫描下面的二维码,关注后回复关键字「电子书」即可获取!

第二种:Gitee:https://gitee.com/zhangbw666/java-books

希望程序员朋友能认真学习,根据自身需要获取对应的电子书,学习是永无止境的。
参考:https://www.cnblogs.com/ysocean/p/8482979.html

你多学一样本事,就少说一句求人的话,现在的努力,是为了以后的不求别人,实力是最强的底气。记住,活着不是靠泪水博得同情,而是靠汗水赢得掌声。
——《写给程序员朋友》

你可能感兴趣的:(工具,Java,面试技巧,Java,面试)