天天看點

Java克隆clone淺拷貝與深拷貝

假設在你的應用中使用一些對象,你如何拷貝你的對象呢?最明顯的方法是講一個對象簡單的指派給另一個,就像這樣:

    obj2 = obj1;

但是這個方法實際上沒有拷貝對象而僅僅是拷貝了一個對象引用,換換言之,在你執行這個操作後仍然隻有一個對象,但是多出了一個對該對象的引用。

如果這個看似明顯的方法不能正常工作,那麼如何實際的拷貝一個對象呢?為什麼不試試Object.clone呢?這個方法對Object的所有子類都是可用的。例如:

    class A {

        private int x;

        public A(int i) {

            x = i;

        }

    }

    public class CloneDemo1 {

        public static void main(String args[])

          throws CloneNotSupportedException {

            A obj1 = new A(37);

            A obj2 = (A)obj1.clone();

        }

    }

這個代碼引發一個編譯錯誤,因為Object.clone是一個protected方法。那麼再試一次,換一種方法:

    class A {

        private int x;

        public A(int i) {

            x = i;

        }

        public Object clone() {

            try {

                return super.clone();

            }

            catch (CloneNotSupportedException e) {

                throw new InternalError(e.toString());

            }

        }

    }

    public class CloneDemo2 {

        public static void main(String args[])

          throws CloneNotSupportedException {

            A obj1 = new A(37);

            A obj2 = (A)obj1.clone();

        }

    }

在這個方法中,呢定義自己的clone方法,它擴充Object.clone方法,CloneDemo2可以編譯,但是當你運作它時會抛出一個CloneNotSupportedException異常。

這裡仍然缺少一些東西,你必須讓那些包含clone方法的類實作Cloneable接口,就像這樣:

    class A implements Cloneable {

        private int x;

        public A(int i) {

            x = i;

        }

        public Object clone() {

            try {

                return super.clone();

            }

            catch (CloneNotSupportedException e) {

                throw new InternalError(e.toString());

            }

        }

        public int getx() {

            return x;

        }

    }

    public class CloneDemo3 {

        public static void main(String args[])

          throws CloneNotSupportedException {

            A obj1 = new A(37);

            A obj2 = (A)obj1.clone();

            System.out.println(obj2.getx());

        }

    }

成功了!CloneDemo3可以編譯并産生期望的結果:

37    

你已經了解了必須顯式的指定clone方法并且你的類必須實作Cloneable接口。Cloneable是“标記”接口的一個範例,接口自身不指定任何東西,但是,Object.clone檢查類是否實作了它,如果沒有就抛出一個CloneNotSupportedException異常。

Object.clone方法做簡單的拷貝操作,将一個對象的所有成員變量拷貝到一個新的對象。在CloneDemo3中,A.clone調用Object.clone,然後Object.clone建立一個新的A對象并将已經存在的那個對象的成員變量的内容拷貝到那個新對象。

CloneDemo3中還有大量其他值得考慮的東西。其中之一就是你可以防止你寫的類的使用者拷貝那個類對象。為了做到這個,你可以不實作Cloneable接口,是以拷貝操作總會抛出異常。然而在大部分情況下為你的類規劃和實作一個clone方法因而可以恰當的拷貝會更好。

另外一點就是你可以支援無條件的和有條件的拷貝。CloneDemo3是無條件的支援拷貝,clone方法不會傳播CloneNotSupportedException異常。

一個更通用的方法是有條件的支援類拷貝。在這種情況下,對象自身可以被拷貝,但是對象的子類可能不能拷貝。對于有條件拷貝,clone方法必須申明它能夠傳播CloneNotSupportedException異常。有條件拷貝的一個範例是一個集合類的對象的元素隻有在那些元素是可以拷貝的時候才能進行拷貝。

有條件拷貝的另外的一種方式是實作一個合适的clone方法但是不實作Cloneable接口。在這種情況下,如果願意,子類可以支援拷貝操作。

拷貝操作可能是很棘手的,因為Object.clone做簡單的對象成員拷貝,有時候這不是你期望的,例如:

    import java.util.*;

    class A implements Cloneable {

        public HashMap map;

        public A() {

            map = new HashMap();

            map.put("key1", "value1");

            map.put("key2", "value2");

        }

        public Object clone() {

            try {

                return super.clone();

            }

            catch (CloneNotSupportedException e) {

                throw new InternalError(e.toString());

            }

        }

    }

    public class CloneDemo4 {

        public static void main(String args[]) {

            A obj1 = new A();

            A obj2 = (A)obj1.clone();

            obj1.map.remove("key1");

            System.out.println(obj2.map.get("key1"));

        }

    }

你可能希望CloneDemo4顯示如下的結果:

value1

但是實際上它顯示:

null

發生了什麼事?在CloneDemo4中,一個對象包含一個HashMap引用,當對象被拷貝時,HashMap 引用也被拷貝了,這意味着拷貝生成的那個對象包含那個HashMap對象的原始引用。是以當原始對象中的HashMap的内容發生變化,拷貝生成的對象中的那個HashMap的内容也同時更新。

要修正這個問題,你可以讓clone方法更完善:

    import java.util.*;

    class A implements Cloneable {

        public HashMap map;

        public A() {

            map = new HashMap();

            map.put("key1", "value1");

            map.put("key2", "value2");

        }

        public Object clone() {

            try {

                A aobj = (A)super.clone();

                aobj.map = (HashMap)map.clone();

                return aobj;

            }

            catch (CloneNotSupportedException e) {

                throw new InternalError(e.toString());

            }

        }

    }

    public class CloneDemo5 {

        public static void main(String args[]) {

            A obj1 = new A();

            A obj2 = (A)obj1.clone();

            obj1.map.remove("key1");

            System.out.println(obj2.map.get("key1"));

        }

    }

Clone5Demo顯示如下的期望的結果:

value1    

Clone5Demo調用super.clone建立一個A對象并拷貝map成員,然後調用HashMap.clone完成HashMap類型的拷貝。這個操作包含建立一個新的hash表并且從老的那個裡面拷貝成員到那個新的hash表。

如果兩個對象共享一個引用,就像CloneDemo4中的情況一樣,那麼通常你會遇到問題,除非那個引用是隻讀的,要避開這個問題,你需要實作clone方法處理這個問題。這種情況的另一種說法是

Object.clone完成的是對象的“淺”拷貝,即簡單的成員到成員的拷貝。它不做“深度”拷貝,即成員或者數組指向的對象的遞歸拷貝。

不使用"new CloneDemo5"建立一個對象,那麼調用super.clone就是極度重要的。你應該在類層次的每一級上調用super.clone。這是因為每一級都可能有它自己的共享對象問題。如果你使用"new"而不是super.clone,那麼你的代碼對于那些從你的類繼承的子類是不正确的,那些代碼調用你的clone方法但是收到一個不正确的傳回類型。

關于拷貝,另一個需要知道的事情是可以拷貝任何數組,隻需簡單的調用clone方法:

    public class CloneDemo6 {

        public static void main(String args[]) {

            int vec1[] = new int[]{1, 2, 3};

            int vec2[] = (int[])vec1.clone();

            System.out.println(vec2[0] + " " + vec2[1] +

                " " + vec2[2]);

        }

    }

關于拷貝的最後一個重要的事情是:它是建立和初始化一個新對象的方式,但是它不同于調用一個構造方法。這個差別的一個例子是它事關空的final成員,也就是那些聲明為"final"但是沒有初始化的成員,它們隻能在構造方法中被指派。下面是一個空的final成員的用法:

    public class CloneDemo7 {

        private int a;

        private int b;

        private final long c;

        public CloneDemo7(int a, int b) {

            this.a = a;

            this.b = b;

            this.c = System.currentTimeMillis();

        }

        public static void main(String args[]) {

            CloneDemo7 obj = new CloneDemo7(37, 47);

        }

    }

在CloneDemo7的構造方法中,一個final成員"c"從系統時鐘中獲得一個時戳。如果你拷貝這樣的類型的值你想得到什麼?Object.clone拷貝所有的成員變量,但是你想那個時戳成員被設定為目前系統時鐘的值。然而,如果一個成員是final類型的,你隻能在構造方法中設定那個成員,不能在clone方法中。下面是這個問題的例子:

    public class CloneDemo8 {

        private int a;

        private int b;

        private final long c;

        public CloneDemo8(int a, int b) {

            this.a = a;

            this.b = b;

            this.c = System.currentTimeMillis();

        }

        public CloneDemo8(CloneDemo8 obj) {

            this.a = obj.a;

            this.b = obj.b;

            this.c = System.currentTimeMillis();

        }

        public Object clone() throws CloneNotSupportedException {

            //this.c = System.currentTimeMillis();

            return super.clone();

        }

        public static void main(String args[]) {

            CloneDemo8 obj = new CloneDemo8(37, 47);

            CloneDemo8 obj2 = new CloneDemo8(obj);

        }

    }

如果你想取消final成員的指派語句那一行的注釋程式就不能編譯。對于這樣的問題,我們不使用clone方法,範例程式使用拷貝構造方法。拷貝構造方法的參數是和它自身類型相同并實作合适的拷貝邏輯。(譯者注:在實作拷貝構造方法時需要注意共享對象問題,由于範例中的其他兩個成員都是原始類型是以沒有問題,但是如果你自己的類的成員的類型是對象類型就不能使用直接指派也要使用拷貝進行或者是其他合适的拷貝構造方法,但是如果你需要使用的類型沒有拷貝方法或者合适的拷貝構造方法,那麼你就不能寫你自己的合适的拷貝構造方法或者拷貝方法,所辛的是java的核心類基本上不存在這個問題,但是你如果使用其他的人的類就不好說了,是以如果你寫自己的類并想讓很多人用,那麼你一定要實作合适的拷貝方法)

也許你認為你可以不使用空final成員而是在聲明那些final成員的時候馬上使用系統時間來初始化解決這樣的問題,就像下面這樣:

    class A implements Cloneable {

        final long x = System.currentTimeMillis();

        public Object clone() {

            try {

                return super.clone();

            }

            catch (CloneNotSupportedException e) {

                throw new InternalError(e.toString());

            }

        }

    }

    public class CloneDemo9 {

        public static void main(String args[]) {

            A obj1 = new A();

            // sleep 100 ms before doing clone,

            // to ensure unique timestamp

            try {

                Thread.sleep(100);

            }

            catch (InterruptedException e) {

                System.err.println(e);

            }

            A obj2 = (A)obj1.clone();

            System.out.println(obj1.x + " " + obj2.x);

        }

    }

這樣同樣不能工作,當你運作這個程式,你可以看到obj1.x和obj2.x有相同的值。這指出當一個對象是拷貝生成的時候通常的對象初始化沒有進行并且你不能在clone方法中設定final成員的值。是以如果簡單的拷貝操作不能正确的初始化一個成員,你就不應該将它聲明為final的。或者你需要使用拷貝構造方法作為拷貝的替代方法。(譯者注:如果你将成員聲明為private并且不提供修改它的值方法,那麼效果和将它聲明為final是相同的)

版權聲明
  評論人:lmgl    參與分: 3    專家分: 0 發表時間: 2002-12-6 上午10:58

為什麼以下的代碼結果是"aaaa"

        String[][] ss=new String[][]{{"342","fs"},{"32","ger"}};

        String[][] s;

        s = (String[][])ss.clone();

        ss[1][1]="aaaa";

        System.out.println(s[1][1]);

  評論人:cherami    參與分: 19726    專家分: 4830 發表時間: 2002-12-6 下午1:27

那是深度拷貝和淺拷貝的問題,請注意文章中的以下内容:

如果兩個對象共享一個引用,就像CloneDemo4中的情況一樣,那麼通常你會遇到問題,除非那個引用是隻讀的,要避開這個問題,你需要實作clone方法處理這個問題。這種情況的另一種說法是

Object.clone完成的是對象的“淺”拷貝,即簡單的成員到成員的拷貝。它不做“深度”拷貝,即成員或者數組指向的對象的遞歸拷貝。

  評論人:kert    參與分: 558    專家分: 520 發表時間: 2002-12-6 下午1:31

Java的Clone應該是克隆對象的值,而不是引用!

然而如果不清楚哪是對象的值,則對clone的結果便會感到疑惑!

看如下的幾個例子!

1.

private static void test1() {

        pl("test1----------");

        String[] s = {"a", "a"};

        String[] ss = (String[]) s.clone();

        pl("s: " + s);

        pl("ss: " + ss);

        ss[0] = "b";

        pl("ss[0]=" + ss[0]);

        pl("s[0]=" + s[0]);

        pl("");

}

我們在這裡clone了一個數組對象,數組的元素是String,是以這裡String是這個s的值。

當我們clone s時,Java生成了兩個同樣的String。并實作了我們想要作的。

輸出為:

[pre]

s: [Ljava.lang.String;@7172ea

ss: [Ljava.lang.String;@2f6684

ss[0]=b

s[0]=a

[/pre]

2.就是同上個回複一樣的例子

private static void test2() {

        pl("test2-----------");

        String[][] s = {

            {"a", "a"},

            {"a", "a"}

        };

        String[][] ss = (String[][]) s.clone();

        pl("s="+s);

        pl("ss="+ss);

        pl("s[0]="+s[0]);

        pl("ss[0]="+ss[0]);

        pl("s[1]="+s[1]);

        pl("ss[1]="+ss[1]);

        ss[0][0]="b";

        pl("s[0][0]="+s[0][0]);

        pl("ss[0][0]="+ss[0][0]);

        pl("");

}

這裡我們想要clone一個二維數組,并且[i]希望[/i]同樣clone數組的每個值。

然而結果卻事與願違;

[pre]

test2-----------

s=[[Ljava.lang.String;@738798

ss=[[Ljava.lang.String;@4b222f

s[0]=[Ljava.lang.String;@3169f8

ss[0]=[Ljava.lang.String;@3169f8

s[1]=[Ljava.lang.String;@2457b6

ss[1]=[Ljava.lang.String;@2457b6

s[0][0]=b

ss[0][0]=b

[/pre]

然而事實上結果卻很好的符合的clone的原則。Java很好的clone了我們的二維數組s。并生成了它的一個副本ss。

而且ss中的每個值(一維數組)與原來的值完全相同。(我們可以從輸出中清楚地看到)。

因為對于一個數組對象而言,數組的元素就是這個對象的值。是以二維數組的元素(值)就是一維數組。

而對于數組對象,Java把它作為類似指針處理。

3.

private static void test3(){

        pl("test3-----------");

        String[][] s = {

            {"a", "a"},

            {"a", "a"}

        };

        String[][]ss={

          (String[]) s[0].clone(),

          (String[]) s[1].clone()

        };

        pl("s="+s);

        pl("ss="+ss);

        pl("s[0]="+s[0]);

        pl("ss[0]="+ss[0]);

        pl("s[1]="+s[1]);

        pl("ss[1]="+ss[1]);

        ss[0][0]="b";

        pl("s[0][0]="+s[0][0]);

        pl("ss[0][0]="+ss[0][0]);

        pl("");

}

而當我們換一種clone方式(深度clone),我們可以看到結果發生了變化

[pre]

test3-----------

s=[[Ljava.lang.String;@7a78d3

ss=[[Ljava.lang.String;@129206

s[0]=[Ljava.lang.String;@30f13d

ss[0]=[Ljava.lang.String;@2e000d

s[1]=[Ljava.lang.String;@55af5

ss[1]=[Ljava.lang.String;@169e11

s[0][0]=a

ss[0][0]=b

[/pre]

---------

對于普通的clone而言(淺度clone),虛拟機使用本地方法,用類似copy記憶體的方式複制一個對象。是以會産生似乎是令人迷惑的結果。

是以通常clone數組或是複雜對象時,需要使用深度clone,一一clone每一個對象的元素。

  評論人:xjw-aaa    參與分: 788    專家分: 330 發表時間: 2003-11-18 下午3:05

cherami sir:

  問題1:

    為什麼數組直接用.clone()方法。而自定義的A類卻用 super.clone()方法。我覺得

用super.clone()方法,不符合面向對象思想。我想克隆的是A類的副本,而super.clone()方法克隆的是父類的對象,然後再用A obj2 = (A)obj1.clone()強制轉換為A類。自定義的A類預設不就是繼承Object類嗎?它本身不就可以直接使用clone()方法嗎,為什麼還用super.clone()方法。

  于是我将super.clone()換成this.clone(),編譯通過。但運作抛出at A.clone(CloneDemo3.java:12)類外,您能不能給我一個合理的解釋。

     class A implements Cloneable 

{      

    private int x; 

    public A(int i) 

    { 

            x = i; 

    }  

    public Object clone() 

    {           

        try {                

            return this.clone();

                           //我将super換成this                      

                           //return super.clone();

            }      

        catch (Exception e)

            {                

            throw new InternalError(e.toString()); 

            }      

    } 

    public int getx() 

    {

        return x;

    }  

}       

    public class CloneDemo3

    {      

        public static void main(String args[])throws Exception 

       {           

        A obj1 = new A(37); 

        A obj2 = (A)obj1.clone();            

        System.out.println(obj2.getx());      

        }    

    }

問題2:

     我想問一下,克隆一個對象是不是給克隆對象從新配置設定一個記憶體空間 ;而引用一個對象是指同一記憶體不同有不同對象名。對象名有點像c++的指針的感覺嗎?