讀古今文學網 > 編寫高質量代碼:改善Java程序的151個建議 > 建議43:避免對象的淺拷貝 >

建議43:避免對象的淺拷貝

我們知道一個類實現了Cloneable接口就表示它具備了被拷貝的能力,如果再覆寫clone()方法就會完全具備拷貝能力。拷貝是在內存中進行的,所以在性能方面比直接通過new生成對像要快很多,特別是在大對象的生成上,這會使性能的提升非常顯著。但是對像拷貝也有一個比較容易忽略的問題:淺拷貝(Shadow Clone,也叫做影子拷貝)存在對像屬性拷貝不徹底的問題。我們來看這樣一段代碼:


public class Client{

public static void main(Stringargs){

//定義父親

Person f=new Person(\"父親\");

//定義大兒子

Person s1=new Person(\"大兒子\",f);

//小兒子的信息是通過大兒子拷貝過來的

Person s2=s1.clone();

s2.setName(\"小兒子\");

System.out.println(s1.getName()+\"的父親是\"+s1.getFather().getName());

System.out.println(s2.getName()+\"的父親是\"+s2.getFather().getName());

}

}

class Person implements Cloneable{

//姓名

private String name;

//父親

private Person father;

public Person(String_name){

name=_name;

}

public Person(String_name, Person_parent){

name=_name;

father=_parent;

}

/*name和parent的getter/setter方法省略*/

//拷貝的實現

@Override

public Person clone(){

Person p=null;

try{

p=(Person)super.clone();

}catch(CloneNotSupportedException e){

e.printStackTrace();

}

return p;

}

}


程序中,我們描述了這樣一個場景:一個父親,有兩個兒子,大小兒子同根同種,所以小兒子對象就通過拷貝大兒子對像來生成,運行輸出的結果如下:


大兒子的父親是父親

小兒子的父親是父親


這很正確,沒有問題。突然有一天,父親心血來潮想讓大兒子去認個乾爹,也就是大兒子的父親名稱需要重新設置一下,代碼如下:


public static void main(Stringargs){

//定義父親

Person f=new Person(\"父親\");

//定義大兒子

Person s1=new Person(\"大兒子\",f);

//小兒子的信息是通過大兒子拷貝過來的

Person s2=s1.clone();

s2.setName(\"小兒子\");

//認乾爹

s1.getFather().setName(\"乾爹\");

System.out.println(s1.getName()+\"的父親是\"+s1.getFather().getName());

System.out.println(s2.getName()+\"的父親是\"+s2.getFather().getName());

}


上面僅僅修改了加粗字體部分,大兒子重新設置了父親名稱,我們期望的輸出是:將大兒子父親的名稱修改為乾爹,小兒子的父親名稱保持不變。下面來檢查一下結果是否如此:


大兒子的父親是乾爹

小兒子的父親是乾爹


怎麼回事,小兒子的父親也成了「乾爹」?兩個兒子都沒有,豈不是要氣死「父親」了!出現這個問題的原因就在於clone方法,我們知道所有類都繼承自Object, Object提供了一個對像拷貝的默認方法,即上面代碼中的super.clone方法,但是該方法是有缺陷的,它提供的是一種淺拷貝方式,也就是說它並不會把對象的所有屬性全部拷貝一份,而是有選擇性的拷貝,它的拷貝規則如下:

(1)基本類型

如果變量是基本類型,則拷貝其值,比如int、float等。

(2)對像

如果變量是一個實例對象,則拷貝地址引用,也就是說此時新拷貝出的對象與原有對象共享該實例變量,不受訪問權限的限制。這在Java中是很瘋狂的,因為它突破了訪問權限的定義:一個private修飾的變量,竟然可以被兩個不同的實例對像訪問,這讓Java的訪問權限體系情何以堪!

(3)String字符串

這個比較特殊,拷貝的也是一個地址,是個引用,但是在修改時,它會從字符串池(String Pool)中重新生成新的字符串,原有的字符串對像保持不變,在此處我們可以認為String是一個基本類型。(有關字符串的知識詳見第4章。)

明白了這三個規則,上面的例子就很清晰了,小兒子對象是通過拷貝大兒子產生的,其父親都是同一個人,也就是同一個對象,大兒子修改了父親名稱,小兒子也就跟著修改了——於是,父親的兩個兒子都沒了!其實要更正也很簡單,clone方法的代碼如下:


public Person clone(){

Person p=null;

try{

p=(Person)super.clone();

p.setFather(new Person(p.getFather().getName()));

}catch(CloneNotSupportedException e){

e.printStackTrace();

}

return p;

}


然後再運行,小兒子的父親就不會是「乾爹」了。如此就實現了對象的深拷貝(Deep Clone),保證拷貝出來的對象自成一體,不受「母體」的影響,和new生成的對象沒有任何區別。

注意 淺拷貝只是Java提供的一種簡單拷貝機制,不便於直接使用。