java克隆之深拷贝与浅拷贝

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

Java深拷贝与浅拷贝实际项目中用的很少,可是对于理解Java中值传递,引用传递十分重要,同时我的认为对于理解内存模型也有帮助,何况面试中也是常常问的,因此理解深拷贝与浅拷贝是十分重要的。面试

1、Java中建立对象的方式数组

①:与构造方法有关的建立对象方式网络

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

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

protected native Object clone() throws CloneNotSupportedException;

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

③:反序列化的方式函数

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

反序列化:能够看作是将对象的二进制流从新读取转换成对象的过程。也就是将在序列化过程当中所生成的二进制串转换成对象的过程。this

序列化的时候咱们能够把一个对象写入到流中,此时原对象还在jvm中,流中的对象能够看做是原对象的一个克隆,以后咱们在经过反序列化操做,就达到了对原对象的一次拷贝。spa

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

此处必须理解,对理解深拷贝,浅拷贝相当重要。

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

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

内存模型:

 

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

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

内存模型:

 

 

3、为何要用克隆?

如今有一个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也改变了呢?对于稍有经验的应该很容易理解,咱们看一下内存模型:

看到了吧,Student s2 = s1这句代码在内存中实际上是使s1,s2指向了同一块内存区域,因此后面s2的操做也影响了s1。

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

4、浅拷贝

克隆实现起来比较简单,被复制的类须要实现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对象并无改变,如今内存模型以下:

堆内存中是有两个对象的,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啊,而且咱们作了克隆技术处理。

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

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

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

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

5、深拷贝

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

 

你们想一下这样一个情节:A对象中有B对象的引用,B对象有C对象的引用,C又有D。。。。,尤为项目中引用三方框架中对象,要是实现深拷贝是否是特别麻烦,全部对象都要实现Cloneable接口,而且重写clone()方法,这样作显然是麻烦的,那怎么更好的处理呢?此时咱们能够利用序列化来实现深拷贝。

6、序列化实现深拷贝

对象序列化是将对象写到流中,反序列化则是把对象从流中读取出来。写到流中的对象则是原始对象的一个拷贝,原始对象还存在 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,北京市东城区

好了到此经过序列化一样实现了深拷贝。

7、克隆的实际应用

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

好了,以上就是关于克隆技术本身的总结,以及最后说了本身工做中用到的情形,本篇到此为止,但愿对你有用。

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

 

相关文章
相关标签/搜索