java克隆之深拷貝與淺拷貝


版權聲明:本文出自汪磊的博客,未經作者允許禁止轉載。

Java深拷貝與淺拷貝實際項目中用的不多,但是對於理解Java中值傳遞,引用傳遞十分重要,同時個人認為對於理解內存模型也有幫助,況且面試中也是經常問的,所以理解深拷貝與淺拷貝是十分重要的。

一、Java中創建對象的方式

①:與構造方法有關的創建對象方式

這是什么意思呢?比如我們new一個對象,其實就是調用對現象的有參或者無參的構造函數,反射中通過Class類的newInstance()方法,這種默認是調用類的無參構造方法創建對象以及Constructor類的newInstance方法,這幾種方式都是直接或者間接利用對象的構造函數來創建對象的。

②:利用Object類中clone()方法來拷貝一個對象,方法定義如下:

protected native Object clone() throws CloneNotSupportedException;

看到了吧還是一個native方法,native方法是非Java語言實現的代碼,通過JNI供Java程序調用。此處有個大體印象就可以了,具體此方法實現是由系統底層來實現的,我們可以在Java層調用此方法來實現拷貝的功能。

③:反序列化的方式

序列化:可以看做是將一個對象轉化為二進制流的過程,通過這種方式把對象存儲到磁盤文件中或者在網絡上傳輸。

反序列化:可以看做是將對象的二進制流重新讀取轉換成對象的過程。也就是將在序列化過程中所生成的二進制串轉換成對象的過程。

序列化的時候我們可以把一個對象寫入到流中,此時原對象還在jvm中,流中的對象可以看作是原對象的一個克隆,之后我們在通過反序列化操作,就達到了對原對象的一次拷貝。

二、Java中基本類型與引用類型說明

此處必須理解,對理解深拷貝,淺拷貝至關重要。

基本類型也叫作值類型,說白了就是一個個簡單的值,charbooleanbyte、short、int、long、float、double都屬於基本類型,基本類型數據引用與數據均存儲在棧區域,比如:

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

內存模型:

 

引用類型包括:類、接口、數組、枚舉等。引用類型數據引用存儲在棧區域,而值則存儲在堆區域,比如:

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

內存模型:

 

 

三、為什么要用克隆?

現在有一個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一個對象,那么對象中屬性都是初始狀態,還需要我們額外調用方法一個個設置比較麻煩,克隆的對象與原對象在堆內存中的地址是不同的,也就是兩個不相干的對象,好了,接下來我們就該看看怎么克隆對象了。

四、淺拷貝

克隆實現起來比較簡單,被復制的類需要實現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引用均指向了同一塊內存地址,那怎么解決這個問題呢?解決這個問題就需要用到深拷貝了。

五、深拷貝

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()方法,這樣做顯然是麻煩的,那怎么更好的處理呢?此時我們可以利用序列化來實現深拷貝。

六、序列化實現深拷貝

對象序列化是將對象寫到流中,反序列化則是把對象從流中讀取出來。寫到流中的對象則是原始對象的一個拷貝,原始對象還存在 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,北京市東城區

好了到此通過序列化同樣實現了深拷貝。

七、克隆的實際應用

工作中很少用到深拷貝這塊知識,我就說一個自己工作中用到的地方,最近寫一個面向對象的網絡請求框架,框架中有一個下載的功能,我們知道下載開始,進度更新,完畢,取消等都有相應的回調,在回調中我會傳遞出去一個下載信息的對象,這個對象包含下載文件的一些信息,比如:總長度,進度,已經下載的大小等等,這個下載信息向外傳遞就用到了克隆,我們只傳遞當前下載信息對象的一個克隆就可以了,千萬別把當前下載信息直接傳遞出去,試想直接傳遞出去,外界要是修改了一些信息咋辦,內部框架是會讀取一些信息的,而我只克隆一份給外界,你只需要知道當前信息就可以了,不用你修改,你要是想修改那隨便也影響不到我內部。

好了,以上就是關於克隆技術自己的總結,以及最后說了自己工作中用到的情形,本篇到此為止,希望對你有用。

聲明:文章將會陸續搬遷到個人公眾號,以后文章也會第一時間發布到個人公眾號,及時獲取文章內容請關注公眾號

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM