Tuesday, October 07, 2008

Java複製(Clone)的應用

簡介:

Java複製(Clone)是Java語言的特性之一,但在實際中應用比較少見。但有時候用複製會更方便更有效率。

對於複製(Clone),Java有一些限制:
1、被複製的類別必須自己實作Cloneable 介面,以指示 Object.clone() 方法可以合法地對該類別實例進行按字段複製。
Cloneable 介面實際上是個標識介面,沒有任何介面方法。
2、實作Cloneable介面的類別應該使用公共方法重寫 Object.clone(它是受保護的)。
某個物件實作了此介面就複製它是不可能的。即使 clone 方法是反射性使用的,也無法保證它將獲得成功。
3、在Java.lang.Object類別中複製方法是這麼定義的:
protected Object clone()
throws CloneNotSupportedException
建立並傳回此物件的一個副本。表明是一個受保護的方法,同一個pakage中可見。
按照慣例,傳回的物件應該通過使用 super.clone 獲得。


引題:

舉個例子說吧,現在有一個物件比如叫foo,你需要在建立當前物件的一個副本作為存根你能怎麼做?

假如你不用Clone,那麼你可以先new一個物件foo1:Foo foo1=new Foo(),
然後用foo給foo1物件set值,這樣就得到foo的副本foo1;除此之外,別無選擇。

這樣說,也許有人會覺得說的過於絕對了,不過事實如此啊。

要產生一個副本,那副本要不要記憶體?----當然要了,那就對了!
既然需要記憶體,(不複製的情況下)你不new還有什麼辦法呢?請大家時刻銘記物件是Java執行時產生的,駐留在計算機記憶體中。

常見錯誤:
下面我澄清幾個初學者容易犯迷糊的錯誤,同樣的問題,產生foo物件的副本:
1、Foo foo1=new Foo();
foo1=foo;
然後就想當然的認為副本foo1生成了!

錯誤原因:foo1沒錯是請求了記憶體,但是執行foo1=foo後,
foo1就不在指向剛請求的記憶體區域了,轉而指向foo物件的記憶體區域,
這時候,foo1、foo指向了同一記憶體區域。
剛才new的操作製造一堆垃圾等著JVM回收。

2、Foo foo1=foo;
錯誤原因:還是兩個變量都指向了同一塊記憶體。

3、有些老鳥更厲害一些:在Foo中定義一個傳回自身的方法:
public Foo getInstance(){
return this;
}

然後,Foo foo1=foo.getInstance();

錯誤原因:同上,主要還是沒有重新開闢記憶體,this在物件裡是什麼?
----就是物件自己的引用!那麼getInstance()自然傳回的就是物件自己,反正又是兩個物件穿了一條褲子

引入複製

看了這麼多方法都不行,還很麻煩!乾脆用複製吧,簡單明了。

廢話不說了,看例子:
定義兩個類別CloneFooA、CloneFooB,然後寫個測試類別CloneDemo分別複製這兩個類別的物件,然後印出測試結果到控制台。


/**
* Created by IntelliJ IDEA.
* User: leizhimin
* Date: 2007-9-20
* Time: 19:40:44
* 簡單類別複製實作
* 要實作複製,必須實作Cloneable介面,這是一個標識介面,沒有介面方法
* 實作了 Cloneable 介面,以指示 Object.clone() 方法可以合法地對該類別實例進行按字段複製。
* 按照慣例,實作此介面的類別應該使用公共方法重寫 Object.clone(它是受保護的)。
*/
public class CloneFooA implements Cloneable {
private String strA;
private int intA;

public CloneFooA(String strA, int intA) {
this.strA = strA;
this.intA = intA;
}

public String getStrA() {
return strA;
}

public void setStrA(String strA) {
this.strA = strA;
}

public int getIntA() {
return intA;
}

public void setIntA(int intA) {
this.intA = intA;
}

/**
* @return 建立並傳回此物件的一個副本。
* @throws CloneNotSupportedException
*/
public Object clone() throws CloneNotSupportedException {
//直接使用父類別的clone()方法,傳回複製副本
return super.clone();
}
}


/**
* Created by IntelliJ IDEA.
* User: leizhimin
* Date: 2007-9-20
* Time: 19:59:55
* 深度複製物件,當類別存在聚合關係的時候,複製就必須考慮聚合物件的複製
*/
public class CloneFooB implements Cloneable {
private CloneFooA fooA;
private double douB;

public CloneFooB(double douB) {
this.douB = douB;
}

public CloneFooB(CloneFooA fooA, double douB) {
this.fooA = fooA;
this.douB = douB;
}

public CloneFooA getFooA() {
return fooA;
}

public void setFooA(CloneFooA fooA) {
this.fooA = fooA;
}

public double getDouB() {
return douB;
}

public void setDouB(double douB) {
this.douB = douB;
}

/**
* 複製操作
*
* @return 自身物件的一個副本
* @throws CloneNotSupportedException
*/
public Object clone() throws CloneNotSupportedException {
//先使用父類別的複製方法進行複製操作
CloneFooB cloneFooB = (CloneFooB) super.clone();
//對於複製後出的物件cloneFooB,如果其成員fooA為null,則不能使用clone(),否則出Null Pointer異常
if (this.fooA != null)
cloneFooB.fooA = (CloneFooA) this.fooA.clone();

return cloneFooB;
}
}


/**
* Created by IntelliJ IDEA.
* User: leizhimin
* Date: 2007-9-20
* Time: 19:52:01
* 測試類別:分別複製CloneFooA和CloneFooB類別,並印出複製前後的結果.
*/
public class CloneDemo {
public static void main(String args[]) throws CloneNotSupportedException {
//CloneFooA複製前
CloneFooA fooA1 = new CloneFooA("FooA", 11);
System.out.println("CloneFooA的物件複製前物件fooA1值為: " + fooA1.getStrA() + "," + fooA1.getIntA());
//CloneFooA複製後
CloneFooA fooA2 = (CloneFooA) fooA1.clone();
System.out.println("CloneFooA的物件複製後物件fooA2值為: " + fooA2.getStrA() + "," + fooA2.getIntA());
//比較fooA1和fooA2記憶體位址
if (fooA1 == fooA2) System.out.println("比較fooA1和fooA2記憶體位址:相等!");
else System.out.println("比較fooA1和fooA2記憶體位址:不相等!");

System.out.println("-------------------------");

//CloneFooB複製前
CloneFooB fooB1 = new CloneFooB(fooA1, new double("33"));
System.out.println("CloneFooB的物件複製前物件fooB1值為: " + fooB1.getFooA().getStrA() + "," + fooB1.getFooA().getIntA() + " | " + fooB1.getDouB());
//CloneFooB複製後
CloneFooB fooB2 = (CloneFooB) fooB1.clone();
System.out.println("CloneFooB的物件複製前物件fooB2值為: " + fooB2.getFooA().getStrA() + "," + fooB2.getFooA().getIntA() + " | " + fooB2.getDouB());

if (fooA1 == fooA2) System.out.println("比較fooB1和fooB2記憶體位址:相等!");
else System.out.println("比較fooB1和fooB2記憶體位址:不相等!");
}
}

執行結果:

CloneFooA的物件複製前物件fooA1值為: FooA,11
CloneFooA的物件複製後物件fooA2值為: FooA,11
比較fooA1和fooA2記憶體位址:不相等!
-------------------------
CloneFooB的物件複製前物件fooB1值為: FooA,11 | 33.0
CloneFooB的物件複製前物件fooB2值為: FooA,11 | 33.0
比較fooB1和fooB2記憶體位址:不相等!

Process finished with exit code 0


反面教材:

最後,我給出我上面提出到最後要給出的反面例子。

隨便寫一個,在CloneFooA 的基礎上做了少許改動,內容如下:

public class CloneFooA implements Cloneable {
private String strA;
private int intA;

public CloneFooA(String strA, int intA) {
this.strA = strA;
this.intA = intA;
}

public String getStrA() {
return strA;
}

public void setStrA(String strA) {
this.strA = strA;
}

public int getIntA() {
return intA;
}

public void setIntA(int intA) {
this.intA = intA;
}

/**
* @return 建立並傳回此物件的一個副本。
* @throws CloneNotSupportedException
*/
public Object clone() throws CloneNotSupportedException {
//直接使用父類別的clone()方法,傳回複製副本
return super.clone();
}

/**
* @return 傳回執行時的物件
*/
public CloneFooA getInstance(){
return this;
}

public static void main(String args[]){
CloneFooA fooA=new CloneFooA("aa",11);
System.out.println(fooA.getStrA()+" "+fooA.getIntA());

CloneFooA fooA1=fooA.getInstance();
System.out.println(fooA1.getStrA()+" "+fooA1.getIntA());
if(fooA==fooA1) System.out.println("fooA和fooA1記憶體位址相等!");

System.out.println("-------------------------");

//改變後fooA或者fooA1中任何一個,看看另外一個是否會改變
fooA1.setStrA("bb");
System.out.println(fooA.getStrA()+" "+fooA.getIntA());
System.out.println(fooA1.getStrA()+" "+fooA1.getIntA());

if(fooA==fooA1) System.out.println("fooA和fooA1記憶體位址相等,改變fooA1後,fooA的值也跟著變化了");
}
}

執行結果:

aa 11
aa 11
fooA和fooA1記憶體位址相等!
-------------------------
bb 11
bb 11
fooA和fooA1記憶體位址相等,改變fooA1後,fooA的值也跟著變化了

Process finished with exit code 0

No comments: