版权声明:本文出自汪磊的博客,未经作者允许禁止转载。

Java深拷贝与浅拷贝实际项目中用的不多,但是对于理解Java中值传递,引用传递十分重要,同时个人认为对于理解内存模型也有帮助,况且面试中也是经常问的,所以理解深拷贝与浅拷贝是十分重要的。

一、Java中创建对象的方式

①:与构造方法有关的创建对象方式

这是什么意思呢?比如我们new一个对象,其实就是调用对现象的有参或者无参的构造函数,反射中通过Class类的newInstance()方法,这种默认是调用类的无参构造方法创建对象以及Constructor类的newInstance方法,这几种方式都是直接或者间接利用对象的构造函数来创建对象的。

②:利用Object类中clone()方法来拷贝一个对象,方法定义如下:

protected native Object clone() throws CloneNotSupportedException;

看到了吧还是一个native方法,native方法是非Java语言实现的代码,通过JNI供Java程序调用。此处有个大体印象就可以了,具体此方法实现是由系统底层来实现的,我们可以在Java层调用此方法来实现拷贝的功能。

③:反序列化的方式

序列化:可以看做是将一个对象转化为二进制流的过程,通过这种方式把对象存储到磁盘文件中或者在网络上传输。

反序列化:可以看做是将对象的二进制流重新读取转换成对象的过程。也就是将在序列化过程中所生成的二进制串转换成对象的过程。

序列化的时候我们可以把一个对象写入到流中,此时原对象还在jvm中,流中的对象可以看作是原对象的一个克隆,之后我们在通过反序列化操作,就达到了对原对象的一次拷贝。

二、Java中基本类型与引用类型说明

此处必须理解,对理解深拷贝,浅拷贝至关重要。

基本类型也叫作值类型,说白了就是一个个简单的值,char、boolean、byte、short、int、long、float、double都属于基本类型,基本类型数据引用与数据均存储在栈区域,比如:

 int a = 100;
int b = 234;

内存模型:

引用类型包括:类、接口、数组、枚举等。引用类型数据引用存储在栈区域,而值则存储在堆区域,比如:

 String c = "abc";
String d = "dgfdere";

内存模型:

三、为什么要用克隆?

现在有一个Student类:

public class Student {

    private int age;

    public void setAge(int age) {
this.age = age;
} public int getAge() {
return age;
}
}

项目中有一个对象复制的需求,并且新对象的改变不能影响原对象。好了,我们撸起来袖子就开始写了,大意如下:

 Student s1 = new Student();
s1.setAge(10);
Student s2 = s1;
System.out.println("s1:"+s1.getAge());
System.out.println("s2:"+s2.getAge());

打印信息如下:

s1:10
s2:10

一看打印信息信心更加爆棚了,完成任务。拿给项目经理看,估计经理直接让你去财务室结算工资了。。。。

上面确实算是复制了,但是后半要求呢?并且新对象的改变不能影响原对象,我们改变代码如下:

Student s1 = new Student();
s1.setAge(10);
Student s2 = s1;
System.out.println("s1:"+s1.getAge());
System.out.println("s2:"+s2.getAge());
//
s2.setAge(12);
System.out.println("s1:"+s1.getAge());
System.out.println("s2:"+s2.getAge());

打印信息如下:

s1:10
s2:10
s1:12
s2:12

咦?怎么s1对象的age也改变了呢?对于稍有经验的应该很容易理解,我们看一下内存模型:

看到了吧,Student s2 = s1这句代码在内存中其实是使s1,s2指向了同一块内存区域,所以后面s2的操作也影响了s1。

那怎么解决这个问题呢?这里就需要用到克隆了,克隆就是克隆一份当前对象并且保存其当前状态,比如当前s1的age是10,那么克隆对象的age同样也是10,相比较我们直接new一个对象这里就是不同点之一,我们直接new一个对象,那么对象中属性都是初始状态,还需要我们额外调用方法一个个设置比较麻烦,克隆的对象与原对象在堆内存中的地址是不同的,也就是两个不相干的对象,好了,接下来我们就该看看怎么克隆对象了。

四、浅拷贝

克隆实现起来比较简单,被复制的类需要实现Clonenable接口,不实现的话在调用对象的clone方法会抛出CloneNotSupportedException异常, 该接口为标记接口(不含任何方法), 覆盖clone()方法,方法中调用super.clone()方法得到需要的复制对象。

接下来我们改造Student类,如下:

 public class Student implements Cloneable {

     private int age;

     public void setAge(int age) {
this.age = age;
} public int getAge() {
return age;
} @Override
protected Object clone() throws CloneNotSupportedException {
//
return super.clone();
}
}

继续改造代码:

 public class Main {

     public static void main(String[] args) {

         try {
Student s1 = new Student();
s1.setAge(10);
Student s2 = (Student) s1.clone();
System.out.println("s1:"+s1.getAge());
System.out.println("s2:"+s2.getAge());
//
s2.setAge(12);
System.out.println("s1:"+s1.getAge());
System.out.println("s2:"+s2.getAge());
} catch (CloneNotSupportedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}

主要就是第8行,调用clone方法来给s2赋值,相当于对s1对象进行了克隆,我们看下打印信息,如下:

s1:10
s2:10
s1:10
s2:12

看到了吧,s2改变其值而s1对象并没有改变,现在内存模型如下:

堆内存中是有两个对象的,s1,s2各自操作自己的对象,互不干涉。好了,到此上面的需求就解决了。

然而过了几天,业务有所改变,需要添加学生的身份信息,信息包含身份证号码以及住址,好吧,我们修改逻辑,新建身份信息类:

 public class IDCardInfo {
//模拟身份证号码
private String number;
//模拟住址
private String address; public String getNumber() {
return number;
} public void setNumber(String number) {
this.number = number;
} public String getAddress() {
return address;
} public void setAddress(String address) {
this.address = address;
} }

很简单,我们继续修改Student类,添加身份信息属性:

 public class Student implements Cloneable {

     private int age;
//添加身份信息属性
private IDCardInfo cardInfo; public void setAge(int age) {
this.age = age;
} public int getAge() {
return age;
} public IDCardInfo getCardInfo() {
return cardInfo;
} public void setCardInfo(IDCardInfo cardInfo) {
this.cardInfo = cardInfo;
} @Override
protected Object clone() throws CloneNotSupportedException {
//
return super.clone();
}
}

以上没什么需要特别解释的,我们运行如下测试:

 public static void main(String[] args) {

         try {

             IDCardInfo card1 = new IDCardInfo();
card1.setNumber("11111111");
card1.setAddress("北京市东城区");
Student s1 = new Student();
s1.setAge(10);
s1.setCardInfo(card1);
Student s2 = (Student) s1.clone();
System.out.println("s1:"+s1.getAge()+","+s1.getCardInfo().getNumber()+","+s1.getCardInfo().getAddress());
System.out.println("s2:"+s2.getAge()+","+s2.getCardInfo().getNumber()+","+s2.getCardInfo().getAddress());
//
card1.setNumber("222222");
card1.setAddress("北京市海淀区");
s2.setAge(12);
System.out.println("s1:"+s1.getAge()+","+s1.getCardInfo().getNumber()+","+s1.getCardInfo().getAddress());
System.out.println("s2:"+s2.getAge()+","+s2.getCardInfo().getNumber()+","+s2.getCardInfo().getAddress());
} catch (CloneNotSupportedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}

主要逻辑就是给s1设置IDCardInfo信息,然后克隆s1对象赋值给s2,接下来改变card1信息,我们看下打印信息:

s1:10,11111111,北京市东城区
s2:10,11111111,北京市东城区
s1:10,222222,北京市海淀区
s2:12,222222,北京市海淀区

咦?怎么又出问题了,我们改变card1的信息,怎么影响了s2对象的身份信息呢?我们想的是只会影响s1啊,并且我们做了克隆技术处理。

到这里又引出两个概念:深拷贝与浅拷贝

以上我们处理的只是浅拷贝,浅拷贝会创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值;如果属性是引用类型,拷贝的就是引用 ,由于拷贝的只是引用而不拷贝其对应的内存对象,所以拷贝后对象的引用类型的属性与原对象引用类型的属性还是指向同一对象,引用类型的属性对应的内存中对象不会拷贝,这里读起来比较绕,好好理解一下。

接下来我们看一下上面例子的内存模型:

看到了吧,就是s1,s2中IDCardInfo引用均指向了同一块内存地址,那怎么解决这个问题呢?解决这个问题就需要用到深拷贝了。

五、深拷贝

Object类中的clone是只能实现浅拷贝的,如果以上浅拷贝理解了,那么深拷贝也不难理解,所谓深拷贝就是将引用类型以及其指向的对象内存区域也一同拷贝一份,而不仅仅拷贝引用。

那怎么实现呢?以上面例子为例,要想实现深拷贝,那么IDCardInfo类也要实现Cloneable接口,并且重写clone()方法,修改如下:

 public class IDCardInfo implements Cloneable{
//模拟身份证号码
private String number;
//模拟住址
private String address; public String getNumber() {
return number;
} public void setNumber(String number) {
this.number = number;
} public String getAddress() {
return address;
} public void setAddress(String address) {
this.address = address;
} @Override
protected Object clone() throws CloneNotSupportedException {
//
return super.clone();
}
}

Student中clone()修改如下:

@Override
protected Object clone() throws CloneNotSupportedException {
//
Student stu = (Student) super.clone();
stu.cardInfo = (IDCardInfo) cardInfo.clone();
return stu;
}

再次运行程序打印如下:

s1:10,11111111,北京市东城区
s2:10,11111111,北京市东城区
s1:10,222222,北京市海淀区
s2:12,11111111,北京市东城区

看到了吧,修改card1信息已经影响不到s2了,到此就实现了对象的深拷贝,此时内存模型如下:

大家想一下这样一个情节:A对象中有B对象的引用,B对象有C对象的引用,C又有D。。。。,尤其项目中引用三方框架中对象,要是实现深拷贝是不是特别麻烦,所有对象都要实现Cloneable接口,并且重写clone()方法,这样做显然是麻烦的,那怎么更好的处理呢?此时我们可以利用序列化来实现深拷贝。

六、序列化实现深拷贝

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

序列化的类都要实现Serializable接口,如果有某个属性不需要序列化,可以将其声明为transient。

接下来我们改造源程序通过序列化来实现深拷贝,IDCardInfo如下:

public class IDCardInfo implements Serializable{

    private static final long serialVersionUID = 7136686765975561495L;
//模拟身份证号码
private String number;
//模拟住址
private String address; public String getNumber() {
return number;
} public void setNumber(String number) {
this.number = number;
} public String getAddress() {
return address;
} public void setAddress(String address) {
this.address = address;
}
}

很简单就是让其实现Serializable接口。

Student改造如下:

public class Student implements Serializable {

    private static final long serialVersionUID = 7436523253790984380L;

    private int age;
//添加身份信息属性
private IDCardInfo cardInfo; public void setAge(int age) {
this.age = age;
} public int getAge() {
return age;
} public IDCardInfo getCardInfo() {
return cardInfo;
} public void setCardInfo(IDCardInfo cardInfo) {
this.cardInfo = cardInfo;
} //实现深拷贝
public Object myClone() 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();
}
}

同样让其实现Serializable接口,并且添加myClone()方法通过序列化反序列化实现其本身的深拷贝。

外部调用myClone()方法就可以实现深拷贝了,如下:

Student s2 = (Student) s1.myClone();

运行程序:

s1:10,11111111,北京市东城区
s2:10,11111111,北京市东城区
s1:10,222222,北京市海淀区
s2:12,11111111,北京市东城区

好了到此通过序列化同样实现了深拷贝。

七、克隆的实际应用

工作中很少用到深拷贝这块知识,我就说一个自己工作中用到的地方,最近写一个面向对象的网络请求框架,框架中有一个下载的功能,我们知道下载开始,进度更新,完毕,取消等都有相应的回调,在回调中我会传递出去一个下载信息的对象,这个对象包含下载文件的一些信息,比如:总长度,进度,已经下载的大小等等,这个下载信息向外传递就用到了克隆,我们只传递当前下载信息对象的一个克隆就可以了,千万别把当前下载信息直接传递出去,试想直接传递出去,外界要是修改了一些信息咋办,内部框架是会读取一些信息的,而我只克隆一份给外界,你只需要知道当前信息就可以了,不用你修改,你要是想修改那随便也影响不到我内部。

好了,以上就是关于克隆技术自己的总结,以及最后说了自己工作中用到的情形,本篇到此为止,希望对你有用。

声明:文章将会陆续搬迁到个人公众号,以后文章也会第一时间发布到个人公众号,及时获取文章内容请关注公众号

java克隆之深拷贝与浅拷贝的更多相关文章

  1. java对象的克隆以及深拷贝与浅拷贝

    一.为什么要使用克隆 在实际编程过程中,我们常常要遇到这种情况:有一个对象A,在某一时刻A中已经包含了一些有效值,此时可能 会需要一个和A完全相同新对象B,并且此后对B任何改动都不会影响到A中的值,也 ...

  2. Java clone() 方法克隆对象——深拷贝与浅拷贝

    基本数据类型引用数据类型特点 1.基本数据类型的特点:直接存储在栈(stack)中的数据 2.引用数据类型的特点:存储的是该对象在栈中引用,真实的数据存放在堆内存里 引用数据类型在栈中存储了指针,该指 ...

  3. 内功心法 -- Java中的深拷贝和浅拷贝

    写在前面的话:读书破万卷,编码如有神--------------------------------------------------------------------这篇博客主要来谈谈" ...

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

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

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

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

  6. java对象克隆以及深拷贝和浅拷贝

    1.什么是"克隆"? 在实际编程过程中,我们常常要遇到这种情况:有一个对象A,在某一时刻A中已经包含了一些有效值,此时可能 会需要一个和A完全相同新对象B,并且此后对B任何改动都不 ...

  7. JAVA中对象的克隆及深拷贝和浅拷贝

    使用场景: 在日常的编程过程 中,经常会遇到,有一个对象OA,在某一时间点OA中已经包含了一些有效值 ,此时可能会需一个和OA完全相对的新对象OB,并且要在后面的操作中对OB的任何改动都不会影响到OA ...

  8. java中的深拷贝与浅拷贝

    Java中对象的创建 clone顾名思义就是复制, 在Java语言中, clone方法被对象调用,所以会复制对象.所谓的复制对象,首先要分配一个和源对象同样大小的空间,在这个空间中创建一个新的对象.那 ...

  9. Java 轻松理解深拷贝与浅拷贝

    目录 前言 直接赋值 拷贝 浅拷贝 举例 原理 深拷贝 实现: Serializable 实现深拷贝 总结 前言 本文代码中有用到一些注解,主要是Lombok与junit用于简化代码. 主要是看到一堆 ...

随机推荐

  1. Nginx负载均衡——扩展功能(NGINX Plus)

    本文主要是介绍了NGINX Plus的相关功能,横跨了NGINX Plus R5/R6/R7/R9等各个不同版本的更新. 什么是NGINX Plus? 顾名思义,就是Nginx的加强版或者扩展版.我们 ...

  2. Linux tar包安装Nginx-1.7.6 (yum方式安装依赖)

    1.首先安装依赖包(依赖包有点多,我们采用yum的方式来安装) yum -y install zlib zlib-devel openssl openssl-devel pcre pcre-devel ...

  3. java web(转)

    装载:http://www.oschina.net/question/12_52027 OSCHINA 软件库有一个分类——Web框架,该分类中包含多种编程语言的将近500个项目. Web框架是开发者 ...

  4. R实战 第五篇:绘图(ggplot2)

    ggplot2包实现了基于语法的.连贯一致的创建图形的系统,由于ggplot2是基于语法创建图形的,这意味着,它由多个小组件构成,通过底层组件可以构造前所未有的图形.ggplot2可以把绘图拆分成多个 ...

  5. linux操作日志:远程登录设置

    想要远程linux服务器,首先需要在服务器上开通ssh服务,安装命令如下: sudo apt-get install openssh-server   在上图的提示中,输入“y”,继续等待安装,安装成 ...

  6. hidden symbol `pthread_atfork'

    gcc交叉编译时发生这种错误 /.. .../voice_demo: hidden symbol `pthread_atfork' in /opt/gcc-linaro-aarch64-linux-g ...

  7. python 对象和json互相转换

    一.python对json的支持 从python2.6开始,python标准库中添加了对json的支持,操作json时,只需要import json即可. 二.python对象转换成json字符串 在 ...

  8. Mysql语句的执行过程

    当你希望MySQL能够以更高的性能运行查询时,最好的办法是弄清楚MySQL是如何优化和执行查询.<高性能MySQL> MySQL客户端与服务器端的通信特点 客户端与服务器之间是半双工通信, ...

  9. 打印机驱动冲突和端口异常:win10更新部分补丁后,打印机本地连接(连接打印机的主机)可以打印,其他共享网络中的电脑可以连接到打印机,但不能打印——解决方案

    一.问题描述: 1.A(WIN10系统)表示连接打印机的电脑,P表示打印机(型号:惠普127M),B(WIN7系统)表示局域网中的电脑 2.A升级后部分补丁后,A可以使用打印机P打印文件,B显示可以连 ...

  10. spring boot多环境配置 直接上代码

    spring:  profiles:    active: test  jackson:    date-format: yyyy-MM-dd HH:mm:ss  datasource:    dri ...