java克隆之深拷贝与浅拷贝

Wesley13
• 阅读 1172

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

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都属于基本类型,基本类型数据引用与数据均存储在栈区域,比如:

1 int a = 100;
2 int b = 234;

内存模型:

java克隆之深拷贝与浅拷贝

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

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

内存模型:

java克隆之深拷贝与浅拷贝

三、为什么要用克隆?

现在有一个Student类:

public class Student {

    private int age;

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

    public int getAge() {
        return age;
    }
}

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

1 Student s1 = new Student();
2 s1.setAge(10);
3 Student s2 = s1;
4 System.out.println("s1:"+s1.getAge());
5 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也改变了呢?对于稍有经验的应该很容易理解,我们看一下内存模型:

java克隆之深拷贝与浅拷贝

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

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

四、浅拷贝

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

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

 1 public class Student implements Cloneable {
 2 
 3     private int age;
 4 
 5     public void setAge(int age) {
 6         this.age = age;
 7     }
 8 
 9     public int getAge() {
10         return age;
11     }
12     
13     @Override
14     protected Object clone() throws CloneNotSupportedException {
15         // 
16         return super.clone();
17     }
18 }

继续改造代码:

 1 public class Main {
 2 
 3     public static void main(String[] args) {
 4         
 5         try {
 6             Student s1 = new Student();
 7             s1.setAge(10);
 8             Student s2 = (Student) s1.clone();
 9             System.out.println("s1:"+s1.getAge());
10             System.out.println("s2:"+s2.getAge());
11             //
12             s2.setAge(12);
13             System.out.println("s1:"+s1.getAge());
14             System.out.println("s2:"+s2.getAge());
15         } catch (CloneNotSupportedException e) {
16             // TODO Auto-generated catch block
17             e.printStackTrace();
18         }
19     }
20 }

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

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

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

java克隆之深拷贝与浅拷贝

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

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

 1 public class IDCardInfo {
 2     //模拟身份证号码
 3     private String number;
 4     //模拟住址
 5     private String address;
 6 
 7     public String getNumber() {
 8         return number;
 9     }
10 
11     public void setNumber(String number) {
12         this.number = number;
13     }
14 
15     public String getAddress() {
16         return address;
17     }
18 
19     public void setAddress(String address) {
20         this.address = address;
21     }
22 
23 }

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

 1 public class Student implements Cloneable {
 2 
 3     private int age;
 4     //添加身份信息属性
 5     private IDCardInfo cardInfo;
 6 
 7     public void setAge(int age) {
 8         this.age = age;
 9     }
10 
11     public int getAge() {
12         return age;
13     }
14     
15     public IDCardInfo getCardInfo() {
16         return cardInfo;
17     }
18 
19     public void setCardInfo(IDCardInfo cardInfo) {
20         this.cardInfo = cardInfo;
21     }
22 
23     @Override
24     protected Object clone() throws CloneNotSupportedException {
25         // 
26         return super.clone();
27     }
28 }

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

 1 public static void main(String[] args) {
 2         
 3         try {
 4             
 5             IDCardInfo card1 = new IDCardInfo();
 6             card1.setNumber("11111111");
 7             card1.setAddress("北京市东城区");
 8             Student s1 = new Student();
 9             s1.setAge(10);
10             s1.setCardInfo(card1);
11             Student s2 = (Student) s1.clone();
12             System.out.println("s1:"+s1.getAge()+","+s1.getCardInfo().getNumber()+","+s1.getCardInfo().getAddress());
13             System.out.println("s2:"+s2.getAge()+","+s2.getCardInfo().getNumber()+","+s2.getCardInfo().getAddress());
14             //
15             card1.setNumber("222222");
16             card1.setAddress("北京市海淀区");
17             s2.setAge(12);
18             System.out.println("s1:"+s1.getAge()+","+s1.getCardInfo().getNumber()+","+s1.getCardInfo().getAddress());
19             System.out.println("s2:"+s2.getAge()+","+s2.getCardInfo().getNumber()+","+s2.getCardInfo().getAddress());
20         } catch (CloneNotSupportedException e) {
21             // TODO Auto-generated catch block
22             e.printStackTrace();
23         }
24     }

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

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

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

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

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

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

java克隆之深拷贝与浅拷贝

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

五、深拷贝

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

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

 1 public class IDCardInfo implements Cloneable{
 2     //模拟身份证号码
 3     private String number;
 4     //模拟住址
 5     private String address;
 6 
 7     public String getNumber() {
 8         return number;
 9     }
10 
11     public void setNumber(String number) {
12         this.number = number;
13     }
14 
15     public String getAddress() {
16         return address;
17     }
18 
19     public void setAddress(String address) {
20         this.address = address;
21     }
22     
23     @Override
24     protected Object clone() throws CloneNotSupportedException {
25         //
26         return super.clone();
27     }
28 }

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了,到此就实现了对象的深拷贝,此时内存模型如下:

java克隆之深拷贝与浅拷贝

大家想一下这样一个情节: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克隆之深拷贝与浅拷贝

点赞
收藏
评论区
推荐文章
翼
3年前
ES6的解构赋值是深拷贝or浅拷贝?
面试时候有面试官问到ES6的解构赋值是深拷贝还是浅拷贝?,这里做一个总结.ES6的解构赋值,大家应该都清楚,就是可以快速取出数组或者对象中的值;我们先来看一个使用案例:更多的解构赋值知识可以查看:https://es6.ruanyifeng.com/docs/destructuring那么,ES6的解构赋值到底是深拷贝还是浅拷贝呢?我们先来看一下深拷贝和浅
Wesley13 Wesley13
3年前
java 复制Map对象(深拷贝与浅拷贝)
java复制Map对象(深拷贝与浅拷贝)CreationTime2018年6月4日10点00分Author:Marydon1.深拷贝与浅拷贝  浅拷贝:只复制对象的引用,两个引用仍然指向同一个对象
待兔 待兔
4个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Java开发面试高频考点学习笔记(每日更新)
Java开发面试高频考点学习笔记(每日更新)1.深拷贝和浅拷贝2.接口和抽象类的区别3.java的内存是怎么分配的4.java中的泛型是什么?类型擦除是什么?5.Java中的反射是什么6.序列化与反序列化7.Object有哪些方法?8.JVM内存模型9.类加载机制10.对象的创建和对象的布局11.Java的四种引用
Java对象拷贝原理剖析及最佳实践
作者:宁海翔1前言对象拷贝,是我们在开发过程中,绕不开的过程,既存在于Po、Dto、Do、Vo各个表现层数据的转换,也存在于系统交互如序列化、反序列化。Java对象拷贝分为深拷贝和浅拷贝,目前常用的属性拷贝工具,包括Apache的
菜园前端 菜园前端
1年前
带你了解JS对象的浅拷贝和深拷贝
以下主要介绍了正常情况下的拷贝、浅拷贝、深拷贝三种方式的区别。正常拷贝:复制一个对象,它们的内存地址是相同的浅拷贝:拷贝对象的第一层属性深拷贝:拷贝对象多层的属性正常拷贝假设我们要复制一个对象,如果不对其进行深拷贝,那么改变其中一个对象后,另外一个对象也会
Souleigh ✨ Souleigh ✨
4年前
实现深拷贝的多种方式
实现深拷贝的多种方式简单来说,深拷贝主要是将另一个对象的属性值拷贝过来之后,另一个对象的属性值并不受到影响,因为此时它自己在堆中开辟了自己的内存区域,不受外界干扰。浅拷贝主要拷贝的是对象的引用值,当改变对象的值,另一个对象的值也会发生变化。1.简单深拷贝(一层浅拷贝)①for循环拷贝//只复制第一层的浅拷贝javascriptfunc
Wesley13 Wesley13
3年前
Java深拷贝和浅拷贝
1.浅复制与深复制概念⑴浅拷贝(浅克隆)   复制出来的对象的所有变量都含有与原来的对象相同的值,而所有的对其他对象的引用仍然指向原来的对象。⑵深拷贝(深克隆)   复制出来的所有变量都含有与原来的对象相同的值,那些引用其他对象的变量将指向复制出来的新对象,而不再是原有的那些被引用的对象。换言之,深复制
Wesley13 Wesley13
3年前
Java中如何克隆集合——ArrayList和HashSet深拷贝
编程人员经常误用各个集合类提供的拷贝构造函数作为克隆List,Set,ArrayList,HashSet或者其他集合实现的方法。需要记住的是,Java集合的拷贝构造函数只提供浅拷贝而不是深拷贝,这意味着存储在原始List和克隆List中的对象是相同的,指向Java堆内存中相同的位置。增加了这个误解的原因之一是对于不可变对象(https:/
Stella981 Stella981
3年前
JavaScript基础心法——深拷贝和浅拷贝
!(https://oscimg.oschina.net/oscnet/c131215a5aaaeb7909d7398688df6ea6dcd.png)浅拷贝和深拷贝都是对于JS中的引用类型而言的,浅拷贝就只是复制对象的引用,如果拷贝后的对象发生变化,原对象也会发生变化。只有深拷贝才是真正地对对象的拷贝。前言说到深浅拷贝,必须先