以前沒有用過 clone() 這個功能,最近因為工作的需要才開始用到
也因此發現在之前寫的程式有一些盲點
先來提提要如何使用 clone()
在一般的情狀下直接呼叫 clone() 會出現 CloneNotSupportedException
若希望這個 Class 是可以被 clone 的,則此類別必須 implements java.lang.Cloneable
只要 implements Cloneable,呼叫 clone() 就可以成功了 (註)
clone() 回傳的物件,所有的屬性值會跟原始物件完全相同
不過請注意到上面的那句話,所有的屬性值會跟原始物件「完全相同」
在 Object.clone() API 裡也有說明到預設的 clone 動作是淺層複製
也就是說,若某個屬性值是類別型態,並不會對此屬性值再去呼叫它的 clone()
所以原始物件與複製出來的物件,其同一個屬性會參照到同一個物件實體
例如
package test.lang; import java.util.ArrayList; public class TestClone1 implements Cloneable { private int i; private ArrayListlist; @Override public String toString() { return "i="+i+",list="+list; } public static void main(String[] args) { TestClone1 o1 = new TestClone1(); o1.i = 3; o1.list = new ArrayList (); o1.list.add("element 1"); TestClone1 o2 = null; try { o2 = (TestClone1) o1.clone(); } catch (CloneNotSupportedException e) { e.printStackTrace(); } System.out.println("o1:"+o1); System.out.println("o2:"+o2); o1.i = 5; o1.list.add("element 2"); System.out.println("o1:"+o1); System.out.println("o2:"+o2); System.out.println("o1.list==o2.list?"+(o1.list==o2.list)); } }
這個例子的執行結果如下
o1:i=3,list=[element 1]
o2:i=3,list=[element 1]
o1:i=5,list=[element 1, element 2]
o2:i=3,list=[element 1, element 2]
o1.list==o2.list?true
我們可以發現,當修改了 o1.i 的值時,o2.i 的值不會跟著變
但當在 o1.list 中加入一個項目後,o2.list 也同時增加了一個項目
最後我們直接用 == 來比對 o1.list 與 o2.list,得到的結果是 true,表示它們真的是同一個實體
若我們查一下 ArrayList 的 API 可以看到它是有 implements Cloneable 的
而這裡仍然是同一個實體,就表示就算屬性的型態有 implements Cloneable 也不會去呼叫它的 clone()
上面的(註),我在這裡來說:
在 Cloneable API 中有提到,Object.clone() 是 protected 的
習慣上,實作 Cloneable 的 Class 應該 override clone() 改成 public
並且改寫成適合該 Class 的 clone 方式
所以上例我們應該改寫 clone() ,並自己去對成員做 clone 的動作,例如
package test.lang; import java.util.ArrayList; public class TestClone1 implements Cloneable { private int i; private ArrayListlist; @Override public String toString() { return "i="+i+",list="+list; } @Override public Object clone() throws CloneNotSupportedException { TestClone1 o = (TestClone1) super.clone(); o.list =(ArrayList ) this.list.clone(); return o; } public static void main(String[] args) { TestClone1 o1 = new TestClone1(); o1.i = 3; o1.list = new ArrayList (); o1.list.add("element 1"); TestClone1 o2 = null; try { o2 = (TestClone1) o1.clone(); } catch (CloneNotSupportedException e) { e.printStackTrace(); } System.out.println("o1:"+o1); System.out.println("o2:"+o2); o1.i = 5; o1.list.add("element 2"); System.out.println("o1:"+o1); System.out.println("o2:"+o2); System.out.println("o1.list==o2.list?"+(o1.list==o2.list)); } }
如此執行結果就會是
o1:i=3,list=[element 1]
o2:i=3,list=[element 1]
o1:i=5,list=[element 1, element 2]
o2:i=3,list=[element 1]
o1.list==o2.list?false
true
這樣就沒問題了
接下來就要來談談我上面所說的盲點了
在之前,我在寫 Inner class (或稱 Nested class) 時,若 Inner class 需要用到主類的成員,我大概都會這樣寫
package test.lang; public class TestClone2 implements Cloneable { private int i; private InnerClass ic; public TestClone2() { this.i = 3; this.ic = new InnerClass(); } private class InnerClass{ private int j; public InnerClass() { this.j = 8; } void print(){ System.out.println("i="+TestClone2.this.i+",j="+this.j); } } public static void main(String[] args) { TestClone2 o1 = new TestClone2(); o1.ic.print(); TestClone2 o2 = null; try { o2 = (TestClone2) o1.clone(); } catch (CloneNotSupportedException e) { e.printStackTrace(); } o2.i = 5; o2.ic.j = 9; o2.ic.print(); } }重點就在於 TestClone2.this.i ,也就是
主類名.this.成員
上面我們雖然明白了 o1.ic 與 o2.ic 會是同一個實體
但是在 InnerClass 中所寫的 TestClone2.this 到底是代表什麼
上例實際執行的結果是
i=3,j=8
i=3,j=9
所以在 InnerClass 中所寫的 TestClone2.this 代表的是 o1
也就是呼叫 InnerClass 的 Constructor 時的那個主類的實體
事實上,下面這一行
this.ic = new InnerClass();
是代表
this.ic = this.new InnerClass();
所以有趣的來了,我們試著改寫一下 clone 如下
package test.lang; public class TestClone2 implements Cloneable { private int i; private InnerClass ic; public TestClone2() { this.i = 3; this.ic = new InnerClass(); } @Override public Object clone() throws CloneNotSupportedException { TestClone2 o = (TestClone2) super.clone(); o.ic = (InnerClass) this.ic.clone(); return o; } private class InnerClass implements Cloneable{ private int j; public InnerClass() { this.j = 8; } void print(){ System.out.println("i="+TestClone2.this.i+",j="+this.j); } @Override public Object clone() throws CloneNotSupportedException { return super.clone(); } } public static void main(String[] args) { TestClone2 o1 = new TestClone2(); o1.ic.print(); TestClone2 o2 = null; try { o2 = (TestClone2) o1.clone(); } catch (CloneNotSupportedException e) { e.printStackTrace(); } o2.i = 5; o2.ic.j = 9; o2.ic.print(); } }
再執行看看
結果卻是
i=3,j=8
i=3,j=9
這是怎麼一回事?
我們試著在 Constructor 上加個 log
...(略)... public TestClone2() { System.out.println("TestClone2 constructor"); this.i = 3; this.ic = new InnerClass(); } ...(略)... public InnerClass() { System.out.println("InnerClass constructor"); this.j = 8; } ...(略)...
再執行看看
結果會是
TestClone2 constructor
InnerClass constructor
i=3,j=8
i=3,j=9
所以我們可以看出,clone() 的動作並不會再去呼叫 Constructor
這樣的情況下就算改寫主類的 clone 去呼叫 InnerClass 的 clone() 也是沒有用的
改善的方式,就是不要直接呼叫 InnerClass 的 clone()
而是要自己處理 clone 的動作
例如
package test.lang; public class TestClone2 implements Cloneable { private int i; private InnerClass ic; public TestClone2() { System.out.println("TestClone2 constructor"); this.i = 3; this.ic = new InnerClass(); } @Override public Object clone() throws CloneNotSupportedException { TestClone2 o = (TestClone2) super.clone(); //下面自己處理 InnerClass 的 clone 動作 o.ic = o.new InnerClass(); // new 前面的 o. 是不可少的 o.ic.j = this.ic.j; return o; } private class InnerClass{ private int j; public InnerClass() { System.out.println("InnerClass constructor"); this.j = 8; } void print(){ System.out.println("i="+TestClone2.this.i+",j="+this.j); } } public static void main(String[] args) { TestClone2 o1 = new TestClone2(); o1.ic.print(); TestClone2 o2 = null; try { o2 = (TestClone2) o1.clone(); } catch (CloneNotSupportedException e) { e.printStackTrace(); } o2.i = 5; o2.ic.j = 9; o2.ic.print(); } }
這樣執行結果就會是
TestClone2 constructor
InnerClass constructor
i=3,j=8
InnerClass constructor
i=5,j=9
另一種方法則是 Inner class 就乾脆改成 static
若在 Inner class 中想使用主類的成員,就要傳參數
例如
package test.lang; public class TestClone3 implements Cloneable { private int i; private InnerClass ic; public TestClone3() { this.i = 3; this.ic = new InnerClass(); } @Override public Object clone() throws CloneNotSupportedException { TestClone3 o = (TestClone3) super.clone(); o.ic = (InnerClass) this.ic.clone(); return o; } private static class InnerClass implements Cloneable{ private int j; public InnerClass() { this.j = 8; } void print(TestClone3 o){ System.out.println("i="+o.i+",j="+this.j); } @Override public Object clone() throws CloneNotSupportedException { return super.clone(); } } public static void main(String[] args) { TestClone3 o1 = new TestClone3(); o1.ic.print(o1); TestClone3 o2 = null; try { o2 = (TestClone3) o1.clone(); } catch (CloneNotSupportedException e) { e.printStackTrace(); } o2.i = 5; o2.ic.j = 9; o2.ic.print(o2); } }
獲益良多
回覆刪除謝謝
Welcome ^ ^
刪除獲益良多
回覆刪除很高興看到原創的教學或心得分享內容
http://myjavawar.blogspot.tw/
交流一下
內容的精純度不如你
但還是希望你能提出指教
不敢當,我這裡只是生活上的一些分享,你那邊比較有專題性,讚哦 ^_^
刪除不好意思, 雖然這是2011年的文章, 但希望您現在還有在看您的部落格,
回覆刪除這邊想跟您請教, 我還是看不懂, 沒被更新到的i是自己的那個class不是內部的class, 為什麼反而是在new一個新的內部class之後, 外部的i就會被更新呢?
謝謝您.
你好,歡迎來到小妖與鴨居的家
刪除看你提出的問題,我想也許你有點誤會了,問題並不是 i 沒被更新,而是顯示的對象錯了,如果我們是這樣來顯示 i:
System.out.println("o1.i="+o1.i);
System.out.println("o2.i="+o2.i);
那麼 i 絕對是正確,因為顯示的 TestClone 對象很明確是我們想要的那一個
而在上例中我們都是呼叫 InnerClass 的 print() 來顯示 TestClone.this 的屬性,而這裡就存在了可能弄錯對象的空間
以 TestClone2 為例,在產生 TestClone2 的動作中
TestClone2 o1 = new TestClone2();
此時在 Constructor 中,InnerClass 也被 new 出實體了:
this.ic = new InnerClass();
但要注意的是,InnerClass 這個類別是 TestClone2 的 "Instance" member,所以上面這個 new InnerClass() 的動作,代表是以 o1 這個 Instance 來產生 InnerClass 實體,上面也有提到,它相當於是這個語法:
this.ic = this.new InnerClass();
而對這個 InnerClass 實體來說(也就是 this.ic ),TestClone2.this 代表的就是 o1,就算 clone 出另一個 InnerClass,對這個新的實體而言(也就是 o2.ic ),TestClone2.this 所代表的仍是 o1,clone() 並不會改變它們的關係
而在修正的版本中,在 TestClone2 的 clone() 裡,它並不是呼叫 InnerClass 的 clone(),而是用下面的語法來產生 InnerClass 實體:
o.ic = o.new InnerClass(); // new 前面的 o. 是不可少的
重點就在於前面的 o.
這代表的就是以新的 o 的 instance member InnerClass 來產生 InnerClass 實體,所以對 o.ic 而言,TestClone2.this 所代表的就是這個新的 o,這樣才能符合我們期待的對象,print 出來的結果就會正確了
不知這樣的解釋,能解答你的疑問嗎?
作者已經移除這則留言。
刪除有點小複雜, 但看了幾次之後好像有點感覺了, 謝謝您! 那意思是, 如果我裡面有多少類別跟多少成員全部都要重新new過跟指定過嗎?
刪除例如我有InnerClass1, InnerClass2, InnerClass
裡面分別又有 x, y, z
全部都需要
o.ic1 = o.new InnerClass1();
o.ic2 = o.new InnerClass2();
o.ic1.x = this.ic1.x,
o.ic1.y = this.ic1.y,
o.ic1.z = this.ic1.z,
全部成員都要做一次吧?
謝謝您!
如果你要用 Instance Member 的方式來寫 InnerClass 的話,除非不會在 InnerClass 內參照到外層的 Class,否則"是的",都要重新 new 過。要不然,就將 InnerClass 改宣告成 static,然後再需要的 method 傳參數進去,這樣就可以不用重新 new 過了。但要注意,不可以選擇在 InnerClass 的 Constructor 傳外層 Class 進去,上面有提過,clone() 不會呼叫 constructor,所以若這樣做,那 clone() 後還是會參照到舊的外層實體
刪除其實你可以用 Debug mode,step by step 跑看看,並一邊觀察屬性值,應該就會更清楚了
刪除太感謝您了, 解釋得很詳細!!!
刪除因為我是Java的新手, 有一些封裝的問題想請教才找到這篇文章, 不曉得方便跟您請教嗎?
謝謝您~
不敢當,新手就能注意到 clone 的議題,還真是厲害,我學 Java 近 10 年才注意到 @_@
刪除有問題可以互相切磋,我也不一定會 :P
想跟您請教的是, 我知道為了達到封裝的目的, 大家常用get, set來存取內部的屬性, 但我在自己練習Java的時候發現一件事情, 似乎是因為Java是傳址的關係, 所以如果類別裡面有建立其他類別, 在get之後, 如果我在外部有做變更, 那就等同於內部那個類別被變更了, 例如.
刪除public class Box{
private ArrayList toy=
new ArrayList();
public String[] getToys(){
return toys.toArray(new String[toys.size()]);
}
....
}
如果有人把getToys後的結果變更了, 例如:
String[] toys = getToys();
Toys[0] = "Car";
這樣第0個玩具就會從本來的玩具變成車了, 而如果內部其實還會去運用到這個物件的話, 那就會整個錯掉, 後來我唯一想到的方法就是, clone 之後再丟出來, 但我總覺得這樣的觀念怪怪的, 想跟您請教, 一般大家封裝都是怎麼做呢?
謝謝您.
我懂你的意思,不過你舉的例子並不會有你說的問題哦,我們來試試
刪除import java.util.ArrayList;
public class Box {
private ArrayList toys = new ArrayList();
public Box() {
this.toys.add("Ball");
}
public String[] getToys() {
return (String[]) this.toys.toArray(new String[this.toys.size()]);
}
public static void main(String[] args) {
Box box = new Box();
System.out.println(box.toys.get(0));
String[] toys = box.getToys();
toys[0] = "Car";
System.out.println(box.toys.get(0));
}
}
執行 Box,結果會輸出
Ball
Ball
並不會改變內部屬性的內容
接著來討論真的會改變的情況
其實首先要了解"值"和"址"的觀念,如果傳的是"值",那麼傳過去的可以說是個複製品,與原來的不會再有關係,若是傳"址",或說傳"參照",那麼傳過去的與原來的都是同一個物件,這個時候,不管哪一方改變這個物件的內容,都會互相影響,這在傳入的參數,以及回傳值,都有這種狀況
目前來說,應該只有傳基本型態時,才會是傳"值",若是傳類別型態,則都是傳"址",但其中要特別注意 String 類別,雖然傳遞 String 是傳"址",但 String 具有不可變的特性,所以一旦改變了它,其實"址"也已經改變,所以不會互相影響
在類別中,要將內容傳到外面,以上面的例子,一般有以下幾種方式
public List getToys1() {
return this.toys;
}
public List getToys2() {
return new ArrayList(this.toys);
}
public String[] getToys3() {
return (String[]) this.toys.toArray(new String[this.toys.size()]);
}
這三者回傳的都是類別型態,所以都是傳址,不管哪一方改變了這個物件的內容,都會互相影響,但是它們還是有一些不同處
在 getToys1() 中,回傳的是 toys 屬性,所以若外面改變了這個 List 的內容,則此屬性內容也會改變
但 getToys2() 是回傳一個全新的 List, 而 getToys3() 則是傳換成一個新的 String[],這些物件已與 toys 屬性沒有關係了,所以就算外面改變了,也不會影響 toys 屬性
不過這也只是就表面來說而已,實務上還要看 List 內存放的是什麼東西,就跟 clone 有淺層及深層複製一樣,如果存的是類別物件,那麼若外面改變的是這個類別物件的內容,那麼在 Box 裡面拿出來用時,也會是改變過後的內容,例如
import java.awt.Color;
import java.util.ArrayList;
import java.util.List;
public class Box {
private List toys = new ArrayList<>();
public Box() {
this.toys.add(new Car(Color.RED));
}
public List getToys1() {
return this.toys;
}
public List getToys2() {
return new ArrayList<>(this.toys);
}
public Car[] getToys3() {
return this.toys.toArray(new Car[this.toys.size()]);
}
public static void main(String[] args) {
Box box = new Box();
System.out.println(box.toys.get(0).getColor());
Car[] toys = box.getToys3();
toys[0].setColor(Color.BLUE);
System.out.println(box.toys.get(0).getColor());
}
public static interface Toy{
public Color getColor();
public void setColor(Color color);
}
public static class Car implements Toy{
private Color color;
public Car(Color color) {
super();
this.color = color;
}
@Override
public Color getColor() {
return this.color;
}
@Override
public void setColor(Color color) {
this.color = color;
}
}
}
執行 Box,結果會輸出
java.awt.Color[r=255,g=0,b=0]
java.awt.Color[r=0,g=0,b=255]
這裡提出這個,例也不是說一定要做到深層複製,這都是要看實務上的應用需要做到什麼事而定的
以上分享供你參考
PS. 提醒你,提出問題之前,最好將 Code 實際跑一遍哦
您好, 非常感謝您的指教, 我的練習因為內容很多又很亂, 也不曉得怎麼貼, 所以才想說舉個簡單的例子, 沒想到還舉錯, 真是非常抱歉, 用了自己都沒試過就拿來發問, 真是不好意思, 我試著截取有問題的部份貼在下面了, 我的狀況跟您最後那個例子應該是一樣的.
刪除我的概念是這樣的, 因為資料庫有表單, 所以我建了一個Table的類別, 類別裡面含有:
ArrayList, 而Field類別含有如下
FieldRule 類別(裡面都是基本型態, 例如isUnSigned...)
ArrayList, 而每個Column類別含有如下
ArrayList<0bject> 當作資料
當然還有其他的基本型態, 其中有幾張表單是已經預建好的, 希望之後會操作可能只會做資料的插入, 查詢跟刪除而已, 所以欄位是固定的, 也就不希望有setField的方法出現, 但是為了可以方便知道或取得這表單裡面有甚麼Field的狀況, 所以還是有getFields的方法, 會回傳Field[], 這時候發現如果getFields後在外部改動取得的Field的內容, 表單裡變原本的Field也會跟著變了, 所以才想說是不是以後get的東西都要clone後再return.
以上當然有把fields設為final的方法, 可是因為我繼承的父類別裡面的field不是final的(心裡其實也不想設成final), 所以就沒有考慮這個方法, 所以想請教return前都把內容clone這個想法在觀念上會很怪嗎? 謝謝您! 以下附上截取的內容.
(當然裡面還有很多奇怪觀念的地方, 例如父類別的table不放setField, 然後繼承下來之後先做TestTable1, 之後再在下一個子類別再加上setField等等的...)
// 以下是預建的 Table 類別
public class TestTable1 extends Table {
// 這個在父類別裡
protected ArrayList = fields;
public TestTable1 (){
super("TestTable1");
setField();
initialize();
}
private void _setFields() {
...... 設定field的內容
}
// 印出欄位
public void printFields(){
for(Field f:fields){
System.out.println(f.getName());
}
System.out.println("");
}
// 取得 field, 這個方法在父類別裡
public Field[] getFields(){
if(fields == null || fields.size() ==0)
{
throw new NoFieldException();
}
return fields.toArray(new
Field[fields.size()]);
}
}
// 以下是試執行的類別
public class Testing {
public static void main(String[] args) {
// TODO Auto-generated method stub
TestTable1 testtable1 =
new TestTable1();
testtable1.printFields();
Field[] fields = null;
try {
fields=testtable1.getFields();
} catch (NoFieldException e) {
e.printStackTrace();
}
fields[0].setName("Hahaha");
testtable1.printFields();
}
}
結果:
Test1_Field1
Test1_Field2
Test1_Field3
Hahaha
Test1_Field2
Test1_Field3
(因為發文時他說不能用0bject的字當內容, 所以我打0Bject)
嗯,你的例子就是深層的問題了,如果這些類別只是自己在用,那自己小心不要越線也就好了,如果是要給別人用,那要根本解決的話,方法一當然就是不要回傳實際的 Field,而是給一個新的,用 clone 或 new 都可以,只是仍是要小心深層問題,方法二是 Field 不要提供 setXxx 之類的 method,必要資訊都在 constructor 傳入即可,或若真有必要時,只開放 package 存取權限給其他同 package 的 class 使用,因為一般來說,給其他人使用時,他們的 package 應該是不同的,那麼就不會有機會去改變 Field 的內容了(除非他很刻意的用同一個 package name)
刪除以上是我的想法,供你參考
好的, 非常感謝您!
刪除我都是土法煉鋼的想例子, 上網找資料, 寫看看這樣的練習也沒學過甚麼資料結果, 所以常常都會覺得自己的觀念好像怪怪的, 但用clone的想法看起來應該沒有錯, 謝謝您!.