关于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;
} }
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 + "]";
} }

  这是一个我们要进行赋值的原始类 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");
}

  打印结果为:

  

  首先看原始类 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:

 package com.ys.test;

 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 属性并没有变化。

  但是这种做法有个弊端,这里我们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();
}

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

Java的深拷贝和浅拷贝的更多相关文章

  1. 细说 Java 的深拷贝和浅拷贝

    版权声明: 本账号发布文章均来自公众号,承香墨影(cxmyDev),版权归承香墨影所有. 未经允许,不得转载. 一.前言 任何变成语言中,其实都有浅拷贝和深拷贝的概念,Java 中也不例外.在对一个现 ...

  2. Java拷贝——深拷贝与浅拷贝

    深拷贝和浅拷贝 值类型 vs 引用类型 在Java中,像数组.类Class.枚举Enum.Integer包装类等等,就是典型的引用类型,所以操作时一般来说采用的也是引用传递的方式: 但是Java的语言 ...

  3. 浅析java的深拷贝与浅拷贝

    (转自:http://www.cnblogs.com/chenssy/p/3308489.html) 首先来看看浅拷贝和深拷贝的定义: 浅拷贝:使用一个已知实例对新创建实例的成员变量逐个赋值,这个方式 ...

  4. Java实现深拷贝和浅拷贝

    1.类实现Cloneable才可以进行对象拷贝 2.Cloneable只实现浅拷贝,需要实现深拷贝的必须要重写clone()方法 3.利用反序列化也可以实现深拷贝,但是反序列化耗时较长 n.浅拷贝是指 ...

  5. 【Java】深拷贝和浅拷贝

    Java中的对象拷贝(Object Copy)指的是将一个对象的所有属性(成员变量)拷贝到另一个有着相同类类型的对象中去.举例说明:比如,对象A和对象B都属于类S,具有属性a和b.那么对对象A进行拷贝 ...

  6. java基础——深拷贝和浅拷贝的区别

    浅拷贝:对基本数据类型进行值传递,对引用数据类型进行引用传递般的拷贝,此为浅拷贝. 深拷贝:对基本数据类型进行值传递,对引用数据类型船舰一个新的对象,并复制内容,这是深拷贝.

  7. Java问题解读系列之IO相关---Java深拷贝和浅拷贝

    前几天和棒棒童鞋讨论Java(TA学的是C++)的时候,他提到一个浅拷贝和深拷贝的问题,当时的我一脸懵圈,感觉自己学Java居然不知道这个知识点,于是今天研究了一番Java中的浅拷贝和深拷贝,下面来做 ...

  8. 浅谈Java中的深拷贝和浅拷贝(转载)

    浅谈Java中的深拷贝和浅拷贝(转载) 原文链接: http://blog.csdn.net/tounaobun/article/details/8491392 假如说你想复制一个简单变量.很简单: ...

  9. 浅谈Java中的深拷贝和浅拷贝

    转载: 浅谈Java中的深拷贝和浅拷贝 假如说你想复制一个简单变量.很简单: int apples = 5; int pears = apples; 不仅仅是int类型,其它七种原始数据类型(bool ...

随机推荐

  1. linux集群架构

    Linux集群架构   根据功能划分为两大类:高可用和负载均衡 高可用集群通常为两台服务器,一台工作,另外一台作为冗余,当提供服务的机器宕机,冗余将接替继续提供服务 实现高可用的开源软件有:heart ...

  2. Nginx:413 Request Entity Too Large解决

    最近在做给博客添加上传PDF的功能,但是在测试上传文件的过程中遇到了413 Request Entity Too Large错误.不过这个无错误是很好解决的,这个错误的出现是因为上传的文件大小超过了N ...

  3. NFS工作原理

    很多同学都知道NFS的使用场景,也知道如何配置和使用,但对NFS的工作原理了解的很少. NFS是C/S模式,首先要有一台服务端跑NFS服务,然后各个客户端直接挂载共享目录使用.NFS服务本身不会监听端 ...

  4. Vue.js源码——事件机制

    写在前面 因为对Vue.js很感兴趣,而且平时工作的技术栈也是Vue.js,这几个月花了些时间研究学习了一下Vue.js源码,并做了总结与输出.文章的原地址:https://github.com/an ...

  5. Python学习/复习神器-->各种方法/技巧在哪用和典型例子(一)

    就我个人在学习Python的过程中,经常会出现学习了新方法后,如果隔上几天不用,就忘了的情况,或者刚学习的更好的方法没有得到应用,还是沿用已有的方法,这样很不利于学习和掌握新姿势,从而拉长学习时间,增 ...

  6. Activiti获取ProcessEngine的三种方法

    1.通过ProcessEngineConfiguration获取 package cn.lonecloud.mavenActivi; import org.activiti.engine.Proces ...

  7. puppeteer,新款headless chrome!

    puppeteer puppeteer是一种谷歌开发的Headless Chrome,因为puppeteer的出现,业内许多自动化测试库停止维护,比如PhantomJS,Selenium IDE fo ...

  8. java9 - 异常处理

    Java异常 1.异常初见 System.out.println(1/0); 运行上面语句之后打印出: Exception in thread "main" java.lang.A ...

  9. 54.1 怎样才算学会django? 知道这28个知识点才算会django2

    学到什么程度才算会django了?这篇文章帮你梳理一下 关于django2的28个不可不知的知识点总结: 1.cookie操作: -客户端本地存储的键值对 2.session操作: -服务器端可以保存 ...

  10. GB 标准

    std::map<int, std::string> GB2261 = { { 0,"未知的性别" }, { 1,"男性" }, { 2," ...