2011年4月17日 星期日

[Java]clone()的使用與注意事項

每個類別都有一個 clone() 的 method,是從 java.lang.Object 繼承而來的
以前沒有用過 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 ArrayList list;
 
 @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 ArrayList list;
 
 @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);
 }
}

17 則留言:

  1. 獲益良多
    很高興看到原創的教學或心得分享內容

    http://myjavawar.blogspot.tw/
    交流一下
    內容的精純度不如你
    但還是希望你能提出指教

    回覆刪除
    回覆
    1. 不敢當,我這裡只是生活上的一些分享,你那邊比較有專題性,讚哦 ^_^

      刪除
  2. 不好意思, 雖然這是2011年的文章, 但希望您現在還有在看您的部落格,
    這邊想跟您請教, 我還是看不懂, 沒被更新到的i是自己的那個class不是內部的class, 為什麼反而是在new一個新的內部class之後, 外部的i就會被更新呢?

    謝謝您.

    回覆刪除
    回覆
    1. 你好,歡迎來到小妖與鴨居的家
      看你提出的問題,我想也許你有點誤會了,問題並不是 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 出來的結果就會正確了

      不知這樣的解釋,能解答你的疑問嗎?

      刪除
    2. 作者已經移除這則留言。

      刪除
    3. 有點小複雜, 但看了幾次之後好像有點感覺了, 謝謝您! 那意思是, 如果我裡面有多少類別跟多少成員全部都要重新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,
      全部成員都要做一次吧?
      謝謝您!

      刪除
    4. 如果你要用 Instance Member 的方式來寫 InnerClass 的話,除非不會在 InnerClass 內參照到外層的 Class,否則"是的",都要重新 new 過。要不然,就將 InnerClass 改宣告成 static,然後再需要的 method 傳參數進去,這樣就可以不用重新 new 過了。但要注意,不可以選擇在 InnerClass 的 Constructor 傳外層 Class 進去,上面有提過,clone() 不會呼叫 constructor,所以若這樣做,那 clone() 後還是會參照到舊的外層實體

      刪除
    5. 其實你可以用 Debug mode,step by step 跑看看,並一邊觀察屬性值,應該就會更清楚了

      刪除
    6. 太感謝您了, 解釋得很詳細!!!
      因為我是Java的新手, 有一些封裝的問題想請教才找到這篇文章, 不曉得方便跟您請教嗎?
      謝謝您~

      刪除
    7. 不敢當,新手就能注意到 clone 的議題,還真是厲害,我學 Java 近 10 年才注意到 @_@
      有問題可以互相切磋,我也不一定會 :P

      刪除
    8. 想跟您請教的是, 我知道為了達到封裝的目的, 大家常用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 之後再丟出來, 但我總覺得這樣的觀念怪怪的, 想跟您請教, 一般大家封裝都是怎麼做呢?

      謝謝您.

      刪除
    9. 我懂你的意思,不過你舉的例子並不會有你說的問題哦,我們來試試

      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 實際跑一遍哦

      刪除
    10. 您好, 非常感謝您的指教, 我的練習因為內容很多又很亂, 也不曉得怎麼貼, 所以才想說舉個簡單的例子, 沒想到還舉錯, 真是非常抱歉, 用了自己都沒試過就拿來發問, 真是不好意思, 我試著截取有問題的部份貼在下面了, 我的狀況跟您最後那個例子應該是一樣的.

      我的概念是這樣的, 因為資料庫有表單, 所以我建了一個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)

      刪除
    11. 嗯,你的例子就是深層的問題了,如果這些類別只是自己在用,那自己小心不要越線也就好了,如果是要給別人用,那要根本解決的話,方法一當然就是不要回傳實際的 Field,而是給一個新的,用 clone 或 new 都可以,只是仍是要小心深層問題,方法二是 Field 不要提供 setXxx 之類的 method,必要資訊都在 constructor 傳入即可,或若真有必要時,只開放 package 存取權限給其他同 package 的 class 使用,因為一般來說,給其他人使用時,他們的 package 應該是不同的,那麼就不會有機會去改變 Field 的內容了(除非他很刻意的用同一個 package name)
      以上是我的想法,供你參考

      刪除
    12. 好的, 非常感謝您!
      我都是土法煉鋼的想例子, 上網找資料, 寫看看這樣的練習也沒學過甚麼資料結果, 所以常常都會覺得自己的觀念好像怪怪的, 但用clone的想法看起來應該沒有錯, 謝謝您!.

      刪除

廣告訊息會被我刪除

Related Posts with Thumbnails