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

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. [APIO2015]八邻旁之桥

    题面在这里 sol 这是一个\(Splay\)的题解 首先,如果一个人的家和办公室在同一侧,我们可以直接预处理; 如果不在同一侧,也可以加上1(当然要过桥啦) 当k==1时 我们设桥的位置为\(pos ...

  2. Chrome 浏览器各版本下载大全【转载】

    随着最近64位版本的 Chrome 浏览器正式版的推出,Chrome 浏览器再次受到广大浏览迷的重点关注,今天我们就整理一下各版本的 Chrome 浏览器 32位及64位的下载地址,方便各位浏览迷选择 ...

  3. css中的注意项,可能会帮助到大家哦!

    CSS样式层叠表 1.link与@import的区别(5点) (1).link为XHTML的标签,可以引进CSS样式表,除了引进CSS文件还可以引进其他的文件如.js或.rss文件;@import为C ...

  4. vmstat结果在不同操作系统上的解释

    vmstat是各种unix上的通用工具,但在不同系统上,结果的解释却不同,最容易混淆的是结果的memory部分: 1.linux: vmstat输出结果中: memory: swapd:已用swap区 ...

  5. C# Redis实战(三)

    三.程序配置 在C# Redis实战(二)中我们安装好了Redis的系统服务,此时Redis服务已经运行. 现在我们需要让我们的程序能正确读取到Redis服务地址等一系列的配置信息,首先,需要在Web ...

  6. java的枚举2

    首先先理解一下java中枚举的本质. java的世界中一切皆是类,下面通过一个例子解释一下enum的本质: package cn.xnchall.enumeration; public class G ...

  7. 使用Angular CLI生成 Angular 5项目

    如果您正在使用angular, 但是没有好好利用angular cli的话, 那么可以看看本文. Angular CLI 官网: https://github.com/angular/angular- ...

  8. 关于在Editplus中设置内容提示比如syso的快捷输出的方法

    在Editplus中默认的内容提示是很少的,比如我们最常用的syso快捷输出就没有,那么怎么来设置呢? 首先打开工具-首选项: 然后打开文件类型及语法-在文件类型中打开Java,如图: 然后打开 我们 ...

  9. python全栈开发-Day4 列表

    python全栈开发-Day4 列表 一.首先按照以下几个点展开列表的学习 #一:基本使用 1 用途 2 定义方式 3 常用操作+内置的方法 #二:该类型总结 1 存一个值or存多个值 只能存一个值 ...

  10. java反射使用及性能比较

    环境准备 package com.lilei.pack09; public class Logger { public void show(){ System.out.println("he ...