Java對象的深複製和淺複製

原文出處:http://www.importnew.com/18999.html

我們在編碼過程經常會碰到將一個對象傳遞給另一個對象,java中對於基本型變量採用的是值傳遞,而對於對象比如bean傳遞時採用的引用傳遞也就是地址傳遞,而很多時候對於對象傳遞我們也希望能夠象值傳遞一樣,使得傳遞之前和之後有不同的內存地址,在這種情況下我們一般採用以下兩種情況。

淺複製與深複製概念

淺複製(淺克隆) :被複制對象的所有變量都含有與原來的對象相同的值,而所有的對其他對象的引用仍然指向原來的對象。換言之,淺複製僅僅複製所考慮的對象,而不復制它所引用的對象。

深複製(深克隆) :被複制對象的所有變量都含有與原來的對象相同的值,除去那些引用其他對象的變量。那些引用其他對象的變量將指向被複制過的新對象,而不再是原有的那些被引用的對象。換言之,深複製把要複製的對象所引用的對象都複製了一遍。

Java的clone()方法

(1)clone方法將對象複製了一份並返回給調用者。一般而言,clone()方法滿足:
①對任何的對象x,都有x.clone() !=x//克隆對象與原對象不是同一個對象;
②對任何的對象x,都有x.clone().getClass()= =x.getClass()//克隆對象與原對象的類型一樣 ;
③如果對象x的equals()方法定義恰當,那麼x.clone().equals(x)應該成立;

(2)Java中對象的克隆:
①爲了獲取對象的一份拷貝,我們可以利用Object類的clone()方法。
②在派生類中覆蓋基類的clone()方法,並聲明爲public。
③在派生類的clone()方法中,調用super.clone()。
④在派生類中實現Cloneable接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package com.king.cloneable;
 
/**
 * 淺複製
 */
public class ShallowStudent implementsCloneable {
    privateString name;
 
    privateintage;
 
    ShallowStudent(String name,intage) {
        this.name = name;
        this.age = age;
    }
 
    publicObject clone() {
        ShallowStudent o =null;
        try{
            // Object中的clone()識別出你要複製的是哪一個對象。
            o = (ShallowStudent)super.clone();
        }catch(CloneNotSupportedException e) {
            System.out.println(e.toString());
        }
        returno;
    }
 
    publicstaticvoid main(String[] args) {
        ShallowStudent s1 =newShallowStudent("zhangsan",18);
        ShallowStudent s2 = (ShallowStudent) s1.clone();
        s2.name ="lisi";
        s2.age =20;
        //修改學生2後,不影響學生1的值。
        System.out.println("name="+ s1.name +"," + "age="+ s1.age);
        System.out.println("name="+ s2.name +"," + "age="+ s2.age);
    }
}

Java的所有類都默認繼承java.lang.Object類,在java.lang.Object類中有一個方法clone()。 JDK API的說明文檔解釋這個方法將返回Object對象的一個拷貝。要說明的有兩點:一是拷貝對象返回的是一個新對象,而不是一個引用。二是拷貝對象與用 new操作符返回的新對象的區別就是這個拷貝已經包含了一些原來對象的信息,而不是對象的初始信息。

上面代碼中有三個值得注意的地方,一是希望能實現clone功能的CloneClass類實現了Cloneable接口,這個接口屬於java.lang包, java.lang包已經被缺省的導入類中,所以不需要寫成java.lang.Cloneable。另一個值得請注意的是重載了clone()方法。最後在clone()方法中調用了super.clone(),這也意味着無論clone類的繼承結構是什麼樣的,super.clone()直接或間接調用了java.lang.Object類的clone()方法。

下面再詳細的解釋一下這幾點
應該說第三點是最重要的,仔細觀察一下Object類的clone()一個native方法,native方法的效率一般來說都是遠高於 java中的非native方法。這也解釋了爲什麼要用Object中clone()方法而不是先new一個類,然後把原始對象中的信息賦到新對象中,雖然這也實現了clone功能。對於第二點,也要觀察Object類中的clone()還是一個protected屬性的方法。這也意味着如果要應用 clone()方法,必須繼承Object類,在Java中所有的類是缺省繼承Object類的,也就不用關心這點了。然後重載clone()方法。還有一點要考慮的是爲了讓其它類能調用這個clone類的clone()方法,重載之後要把clone()方法的屬性設置爲public。

那麼clone類爲什麼還要實現Cloneable接口呢?稍微注意一下,Cloneable接口是不包含任何方法的!其實這個接口僅僅是一個標誌,而且這個標誌也僅僅是針對Object類中clone()方法的,如果clone類沒有實現Cloneable接口,並調用了Object的 clone()方法(也就是調用了super.Clone()方法),那麼Object的clone()方法就會拋出 CloneNotSupportedException異常。

說明:
①爲什麼我們在派生類中覆蓋Object的clone()方法時,一定要調用super.clone()呢?在運行時刻,Object中的clone()識別出你要複製的是哪一個對象,然後爲此對象分配空間,並進行對象的複製,將原始對象的內容一一複製到新對象的存儲空間中。
②繼承自java.lang.Object類的clone()方法是淺複製。以下代碼可以證明之。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
package com.king.cloneable;
 
/**
 * 淺複製2
 */
public class ShallowStudent2 implementsCloneable {
    String name;// 常量對象。
    intage;
    Professor p;// 學生1和學生2的引用值都是一樣的。
 
    ShallowStudent2(String name,intage, Professor p) {
        this.name = name;
        this.age = age;
        this.p = p;
    }
 
    publicObject clone() {
        ShallowStudent2 o =null;
        try{
            o = (ShallowStudent2)super.clone();
        }catch(CloneNotSupportedException e) {
            System.out.println(e.toString());
        }
        returno;
    }
 
    publicstaticvoid main(String[] args) {
        Professor p =newProfessor("wangwu",50);
        ShallowStudent2 s1 =newShallowStudent2("zhangsan",18, p);
        ShallowStudent2 s2 = (ShallowStudent2) s1.clone();
        s2.p.name ="lisi";
        s2.p.age =30;
        System.out.println("name="+ s1.p.name +","+ "age="+ s1.p.age);
        System.out.println("name="+ s2.p.name +","+ "age="+ s2.p.age);
        //輸出結果學生1和2的教授成爲lisi,age爲30。
    }
}
 
class Professor {
    String name;
    intage;
 
    Professor(String name,intage) {
        this.name = name;
        this.age = age;
    }
}

從中可以看出,調用Object類中clone()方法產生的效果是:先在內存中開闢一塊和原始對象一樣的空間,然後原樣拷貝原始對象中的內容。對基本數據類型,這樣的操作是沒有問題的,但對非基本類型變量,我們知道它們保存的僅僅是對象的引用,這也導致clone後的非基本類型變量和原始對象中相應的變量指向的是同一個對象。大多時候,這種clone的結果往往不是我們所希望的結果,這種clone也被稱爲”影子clone”。

那應該如何實現深層次的克隆,即修改s2的教授不會影響s1的教授?代碼改進如下。 改進使學生1的Professor不改變(深層次的克隆):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
package com.king.cloneable;
 
/**
 * 深複製
 */
public class DeepStudent implementsCloneable {
    String name;// 常量對象。
    intage;
    DeepProfessor p;// 學生1和學生2的引用值都是一樣的。
 
    DeepStudent(String name,intage, DeepProfessor p) {
        this.name = name;
        this.age = age;
        this.p = p;
    }
 
    publicObject clone() {
        DeepStudent o =null;
        try{
            o = (DeepStudent)super.clone();
            //對引用的對象也進行復制
            o.p = (DeepProfessor) p.clone();
        }catch(CloneNotSupportedException e) {
            System.out.println(e.toString());
        }
        returno;
    }
 
    publicstaticvoid main(String[] args) {
        DeepProfessor p =newDeepProfessor("wangwu",50);
        DeepStudent s1 =newDeepStudent("zhangsan",18, p);
        DeepStudent s2 = (DeepStudent) s1.clone();
        s2.p.name ="lisi";
        s2.p.age =30;
        System.out.println("name="+ s1.p.name +","+ "age="+ s1.p.age);
        System.out.println("name="+ s2.p.name +","+ "age="+ s2.p.age);
        //輸出結果學生1和2的教授成爲lisi,age爲30。
    }
}
 
class DeepProfessor implements Cloneable {
    String name;
    intage;
 
    DeepProfessor(String name,intage) {
        this.name = name;
        this.age = age;
    }
 
    publicObject clone() {
        DeepProfessor o =null;
        try{
            o = (DeepProfessor)super.clone();
        }catch(CloneNotSupportedException e) {
            System.out.println(e.toString());
        }
        returno;
    }
}

JDK中StringBuffer類型,關於StringBuffer的說明,StringBuffer沒有重載clone()方法,更爲嚴重的是StringBuffer還是一個 final類,這也是說我們也不能用繼承的辦法間接實現StringBuffer的clone。如果一個類中包含有StringBuffer類型對象或和 StringBuffer相似類的對象,我們有兩種選擇:要麼只能實現影子clone,要麼就在類的clone()方法中加一句(假設是 SringBuffer對象,而且變量名仍是p): o.p = new StringBuffer(p.toString()); //原來的是:o.p = (DeepProfessor) p.clone();

還要知道的是除了基本數據類型能自動實現深度clone以外,String對象是一個例外,它clone後的表現好象也實現了深度clone,雖然這只是一個假象,但卻大大方便了我們的編程。

通過以上我們可以看出在某些情況下,我們可以利用clone方法來實現對象的深度複製,但對於比較複雜的對象(比如對象中包含其他對象,其他對象又包含別的對象…..)這樣我們必須進行層層深度clone,每個對象需要實現cloneable接口,比較麻煩,那就繼續學習下一個序列化方法。

利用串行化來做深複製

所謂對象序列化就是將對象的狀態轉換成字節流,以後可以通過這些值再生成相同狀態的對象。這個過程也可以通過網絡實現,可以先在Windows機器上創建一個對象,對其序列化,然後通過網絡發給一臺Unix機器,然後在那裏準確無誤地重新?裝配?。是不是很神奇。

也許你會說,只瞭解一點點,但從來沒有接觸過,其實未必如此。RMI、Socket、JMS、EJB你總該用過一種吧,彼此爲什麼能夠傳遞Java對象,當然都是對象序列化機制的功勞。
第一次使用Java的對象序列化是做某項目,當時要求把幾棵非常複雜的樹(JTree)及相應的數據保存下來(就是我們常用的保存功能),以便下次運行程序時可以繼續上次的操作。
那時XML技術在網上非常的熱,而且功能也強大,再加上樹的結構本來就和XML存儲數據的格式很像。作爲一項對新技術比較有興趣的我當然很想嘗試一下。不過經過仔細分析,發現如果採用XML保存數據,後果真是難以想象:哪棵樹的哪個節點被展開、展開到第幾級、節點當前的屬性是什麼。真是不知該用A、B、C還是用1、2、3來表示。

還好,發現了Java的對象序列化機制,問題迎刃而解,只需簡單的將每棵樹的根節點序列化保存到硬盤上,下次再通過反序列化後的根節點就可以輕鬆的構造出和原來一模一樣的樹來。

串行化的概念和目的

1.什麼是串行化

對象的壽命通常隨着生成該對象的程序的終止而終止。有時候,可能需要將對象的狀態保存下來,在需要時再將對象恢復。我們把對象的這種能記錄自己的狀態以便將來再生的能力,叫作對象的持續性(persistence)。對象通過寫出描述自己狀態的數值來記錄自己 ,這個過程叫對象的串行化(Serialization) 。串行化的主要任務是寫出對象實例變量的數值。如果變量是另一對象的引用,則引用的對象也要串行化。這個過程是遞歸的,串行化可能要涉及一個複雜樹結構的單行化,包括原有對象、對象的對象、對象的對象的對象等等。對象所有權的層次結構稱爲圖表(graph)。

2.串行化的目的

Java對象的串行化的目標是爲Java的運行環境提供一組特性,如下所示:

1) 儘量保持對象串行化的簡單扼要 ,但要提供一種途徑使其可根據開發者的要求進行擴展或定製。

2) 串行化機制應嚴格遵守Java的對象模型 。對象的串行化狀態中應該存有所有的關於種類的安全特性的信息。

3) 對象的串行化機制應支持Java的對象持續性。

4) 對象的串行化機制應有足夠的 可擴展能力以支持對象的遠程方法調用(RMI)。

5) 對象串行化應允許對象定義自身的格式即其自身的數據流表示形式,可外部化接口來完成這項功能。


其實保存數據,尤其是複雜數據的保存正是對象序列化的典型應用。最近另一個項目就遇到了需要對非常複雜的數據進行存取,通過使用對象的序列化,問題同樣化難爲簡。

對象的序列化還有另一個容易被大家忽略的功能就是對象複製(Clone),Java中通過Clone機制可以複製大部分的對象,但是衆所周知,Clone有深層Clone和淺層Clone,如果你的對象非常非常複雜,假設有個100層的Collection(誇張了點),如果你想實現深層 Clone,真是不敢想象,如果使用序列化,不會超過10行代碼就可以解決。

主要是爲了避免重寫比較複雜對象的深複製的clone()方法,也可以程序實現斷點續傳等等功能。把對象寫到流裏的過程是串行化(Serilization)過程,但是在Java程序師圈子裏又非常形象地稱爲“冷凍”或者“醃鹹菜(picking)”過程;而把對象從流中讀出來的並行化(Deserialization)過程則叫做 “解凍”或者“回鮮(depicking)”過程。

應當指出的是,寫在流裏的是對象的一個拷貝,而原對象仍然存在於JVM裏面,因此“醃成鹹菜”的只是對象的一個拷貝,Java鹹菜還可以回鮮。

在Java語言裏深複製一個對象,常常可以先使對象實現Serializable接口,然後把對象(實際上只是對象的一個拷貝)寫到一個流裏(醃成鹹菜),再從流裏讀出來(把鹹菜回鮮),便可以重建對象。

如下爲深複製源代碼:

1
2
3
4
5
6
7
8
9
10
public Object deepClone() {   
   //將對象寫到流裏   
   ByteArrayOutoutStream bo=newByteArrayOutputStream();   
   ObjectOutputStream oo=newObjectOutputStream(bo);   
   oo.writeObject(this);   
   //從流裏讀出來   
   ByteArrayInputStream bi=newByteArrayInputStream(bo.toByteArray());   
   ObjectInputStream oi=newObjectInputStream(bi);   
   return(oi.readObject());   
}

這樣做的前提是對象以及對象內部所有引用到的對象都是可串行化的,否則,就需要仔細考察那些不可串行化的對象或屬性可否設成transient,從而將之排除在複製過程之外。上例代碼改進如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
package com.king.cloneable;
 
import java.io.*;
 
/**
 * 通過串行化實現深複製
 */
class Teacher implements Serializable {
    String name;
    intage;
 
    publicTeacher(String name,intage) {
        this.name = name;
        this.age = age;
    }
}
 
public class DeepStudent2 implementsSerializable {
    String name;//常量對象
    intage;
    Teacher t;//學生1和學生2的引用值都是一樣的。
 
    publicDeepStudent2(String name,intage, Teacher t) {
        this.name = name;
        this.age = age;
        this.t = t;
    }
 
    publicObject deepClone()throwsIOException, ClassNotFoundException {//將對象寫到流裏
        ByteArrayOutputStream bo =newByteArrayOutputStream();
        ObjectOutputStream oo =newObjectOutputStream(bo);
        oo.writeObject(this);//從流裏讀出來
        ByteArrayInputStream bi =newByteArrayInputStream(bo.toByteArray());
        ObjectInputStream oi =newObjectInputStream(bi);
        return(oi.readObject());
    }
 
    publicstaticvoid main(String[] args)throws IOException, ClassNotFoundException {
        Teacher t =newTeacher("tangliang",30);
        DeepStudent2 s1 =newDeepStudent2("zhangsan",18, t);
        DeepStudent2 s2 = (DeepStudent2) s1.deepClone();
        s2.t.name ="tony";
        s2.t.age =40;
        //學生1的老師不改變
        System.out.println("name="+ s1.t.name +","+ "age="+ s1.t.age);
    }
}

雖然Java的序列化非常簡單、強大,但是要用好,還有很多地方需要注意。比如曾經序列化了一個對象,可由於某種原因,該類做了一點點改動,然後重新被編譯,那麼這時反序列化剛纔的對象,將會出現異常。

你可以通過添加serialVersionUID屬性來解決這個問題。如果你的類是個單態(Singleton)類,是否允許用戶通過序列化機制複製該類,如果不允許你需要謹慎對待該類的實現。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章