一篇文章讲明白Java的深拷贝和浅拷贝

简介: 一篇文章讲明白Java的深拷贝和浅拷贝

目录

1、创建对象的5种方式3、Clone 方法4、基本类型和引用类型5、浅拷贝6、深拷贝7、如何实现深拷贝?  ①、让每个引用类型属性内部都重写clone() 方法  ②、利用序列化资料推荐

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

回到顶部1、创建对象的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关键字有一篇博客专门有介绍,不理解也没关系,只需要知道用 native 修饰的方法就是告诉操作系统,这个方法我不实现了,让操作系统去实现。具体怎么实现我们不需要了解,只需要知道 clone方法的作用就是复制对象,产生一个新的对象。那么这个新的对象和原对象是什么关系呢?

回到顶部4、基本类型和引用类型

  这里再给大家普及一个概念,在 Java 中基本类型和引用类型的区别。

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

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

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

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

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

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

回到顶部5、浅拷贝

  我们看如下这段代码:

package com.ys.test;

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;

}

}

View Code

package com.ys.test;

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 + "】";

}

}

View Code

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

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

  测试:

@Test

public void testShallowClone() 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");

}

View Code

  打印结果为:

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

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

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

  p1:com.ys.test.Person@349319f9

  p2:com.ys.test.Person@258e4566

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

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

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

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

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

回到顶部6、深拷贝

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

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

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

回到顶部7、如何实现深拷贝?

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

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

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

  Address.class:

1 package com.ys.test;

2

3 public class Address implements Cloneable{

4 private String provices;

5 private String city;

6 public void setAddress(String provices,String city){

7 this.provices = provices;

8 this.city = city;

9 }

10 @Override

11 public String toString() {

12 return "Address 【provices=" + provices + ", city=" + city + "】";

13 }

14 @Override

15 protected Object clone() throws CloneNotSupportedException {

16 return super.clone();

17 }

18

19 }

View Code

  Person.class 的 clone() 方法:

1 @Override

2 protected Object clone() throws CloneNotSupportedException {

3 Person p = (Person) super.clone();

4 p.address = (Address) address.clone();

5 return p;

6 }

View Code

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

  但是这种做法有个弊端,这里我们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 //代码效果参考:http://www.zidongmutanji.com/bxxx/525444.html

ois.readObject();

}

View Code

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

回到顶部资料推荐

最近又赶上跳槽的高峰期(招聘旺季),好多读者都问我要有没有最新面试题,找华为朋友整理一份内部资料《第6版:互联网大厂面试题》!

整个资料包,包括 Spring、Spring Boot/Cloud、Dubbo、JVM、集合、多线程、JPA、MyBatis、MySQL、大数据、Nginx、Git、Docker、GitHub、Servlet、JavaWeb、IDEA、Redis、算法、面试题等几乎覆盖了 Java 基础和阿里巴巴等大厂面试题等、等技术栈!

另外,还有博主为大家整理的Java关键字解析、Java源码解析

据说已经有小伙伴通过这套资料,成功的入职了蚂蚁金服、字节跳动等大厂。

而且,这些资料不是扫描版的,里面的文字都可以直接复制,非常便于我们学习:

如果你想获得完整PDF可以通过以下方式获得

面试大全怎么获取:

关注下方公众号

在下方公众号后台回复 【666】 即可。

作者:IT可乐

出处:

本文版权归作者所有,欢迎转载,但未经作者同意不能转载,否则保留追究法律责任的权利。

相关文章
|
2月前
|
存储 安全 Java
从入门到精通:Java Map全攻略,一篇文章就够了!
【10月更文挑战第17天】本文详细介绍了Java编程中Map的使用,涵盖Map的基本概念、创建、访问与修改、遍历方法、常用实现类(如HashMap、TreeMap、LinkedHashMap)及其特点,以及Map在多线程环境下的并发处理和性能优化技巧,适合初学者和进阶者学习。
72 3
|
4月前
|
前端开发 Java 编译器
【前端学java】java基础练习缺少项目?看这篇文章就够了!(完结)
【8月更文挑战第11天】java基础练习缺少项目?看这篇文章就够了!(完结)
46 0
|
2月前
|
存储 安全 Java
从入门到精通:Java Map全攻略,一篇文章就够了!
【10月更文挑战第19天】本文介绍了Java编程中重要的数据结构——Map,通过问答形式讲解了Map的基本概念、创建、访问与修改、遍历方法、常用实现类(如HashMap、TreeMap、LinkedHashMap)及其特点,以及Map在多线程环境下的使用和性能优化技巧,适合初学者和进阶者学习。
86 4
|
4月前
|
存储 前端开发 JavaScript
【前端学java】一篇文章带你精通java 中的数组(10)
【8月更文挑战第10天】一篇文章带你精通java 中的数组
19 3
【前端学java】一篇文章带你精通java 中的数组(10)
|
4月前
|
JavaScript 前端开发 Java
java高质量数据流概念讲解,保证一篇文章帮助你搞懂概念!
【8月更文挑战第11天】java高质量数据流概念讲解,保证一篇文章帮助你搞懂概念!
37 0
java高质量数据流概念讲解,保证一篇文章帮助你搞懂概念!
|
4月前
|
缓存 前端开发 JavaScript
一篇文章助你搞懂java中的线程概念!纯干货,快收藏!
【8月更文挑战第11天】一篇文章助你搞懂java中的线程概念!纯干货,快收藏!
41 0
|
4月前
|
前端开发 Java 编译器
【前端学java】java基础练习缺少项目?看这篇文章就够了!(17)
【8月更文挑战第11天】java基础练习缺少项目?看这篇文章就够了!
37 0
|
4月前
|
Java
Java 新手入门:重载和重写傻傻分不清?一篇文章带你清晰理解!
Java 新手入门:重载和重写傻傻分不清?一篇文章带你清晰理解!
43 0
Java 新手入门:重载和重写傻傻分不清?一篇文章带你清晰理解!
|
6月前
|
Java
一篇文章讲明白Java中内部类详解—匿名内部类
一篇文章讲明白Java中内部类详解—匿名内部类
107 2
|
6月前
|
XML Java 数据格式
一篇文章讲明白java的常用定时任务的几种方式
一篇文章讲明白java的常用定时任务的几种方式
49 0