探究Java中的克隆

myhome1998 8年前發布 | 10K 次閱讀 Java Java開發

克隆,想必大家都有耳聞,世界上第一只克隆羊多莉就是利用細胞核移植技術將哺乳動物的成年體細胞培育出新個體,甚為神奇。其實在Java中也存在克隆的概念,即實現對象的復制。

本文將嘗試介紹一些關于Java中的克隆和一些深入的問題,希望可以幫助大家更好地了解克隆。

Java中的賦值

在Java中,賦值是很常用的,一個簡單的賦值如下

//原始類型
int a = 1;
int b = a;

//引用類型
String[] weekdays = new String[5];
String[] gongzuori = weekdays;//僅拷貝引用

在上述代碼中。

  • 如果是原始數據類型,賦值傳遞的為真實的值
  • 如果是引用數據類型,賦值傳遞的為對象的引用,而不是對象。

了解了數據類型和引用類型的這個區別,便于我們了解clone。

Clone

在Java中,clone是將已有對象在內存中復制出另一個與之相同的對象的過程。java中的克隆為逐域復制。

在Java中想要支持clone方法, 需要首先實現Cloneable接口

Cloneable其實是有點奇怪的,它不同與我們常用到的接口,它內部不包含任何方法,它僅僅是一個標記接口。

其源碼如下

public interface Cloneable {
}

關于cloneable,需要注意的

  • 如果想要支持clone,就需要實現Cloneable 接口
  • 如果沒有實現Cloneable接口的調用clone方法,會拋出CloneNotSupportedException異常。

然后是重寫clone方法,并修改成public訪問級別

static class CloneableImp implements Cloneable {
  public int count;
  public Child child;


  @Override
  public Object clone() throws CloneNotSupportedException {
      return super.clone();
  }
}

調用clone方法復制對象

CloneableImp imp1 = new CloneableImp();
imp1.child = new Child("Andy");
try {
  Object obj = imp1.clone();
  CloneableImp imp2 = (CloneableImp)obj;
  System.out.println("main imp2.child.name=" + imp2.child.name);
} catch (CloneNotSupportedException e) {
  e.printStackTrace();
}

淺拷貝

上面的代碼實現的clone實際上是屬于淺拷貝(Shallow Copy)。

關于淺拷貝,你該了解的

  • 使用默認的clone方法
  • 對于原始數據域進行值拷貝
  • 對于引用類型僅拷貝引用
  • 執行快,效率高
  • 不能做到數據的100%分離。
  • 如果一個對象只包含原始數據域或者不可變對象域,推薦使用淺拷貝。

關于無法做到數據分離,我們可以使用這段代碼驗證

CloneableImp imp1 = new CloneableImp();
imp1.child = new Child("Andy");
try {
  Object obj = imp1.clone();
  CloneableImp imp2 = (CloneableImp)obj;
  imp2.child.name = "Bob";

  System.out.println("main imp1.child.name=" + imp1.child.name);
} catch (CloneNotSupportedException e) {
  e.printStackTrace();
}

上述代碼我們使用了imp1的clone方法克隆出imp2,然后修改 imp2.child.name 為 Bob,然后打印imp1.child.name 得到的結果是

main imp1.child.name=Bob

原因是淺拷貝并沒有做到數據的100%分離,imp1和imp2共享同一個Child對象,所以一個修改會影響到另一個。

深拷貝

深拷貝可以解決數據100%分離的問題。只需要對上面代碼進行一些修改即可。

  1. Child實現Cloneable接口。
public class Child implements  Cloneable{

  public String name;

  public Child(String name) {
      this.name = name;
  }

  @Override
  public String toString() {
      return "Child [name=" + name + "]";
  }

  @Override
  protected Object clone() throws CloneNotSupportedException {
      return super.clone();
  }
}

2.重寫clone方法,調用數據域的clone方法。

static class CloneableImp implements Cloneable {
  public int count;
  public Child child;


  @Override
  public Object clone() throws CloneNotSupportedException {
      CloneableImp obj = (CloneableImp)super.clone();
      obj.child = (Child) child.clone();
      return obj;
  }
}

當我們再次修改imp2.child.name就不會影響到imp1.child.name的值了,因為imp1和imp2各自擁有自己的child對象,因為做到了數據的100%隔離。

關于深拷貝的一些特點

  • 需要重寫clone方法,不僅僅只調用父類的方法,還需調用屬性的clone方法
  • 做到了原對象與克隆對象之間100%數據分離
  • 如果是對象存在引用類型的屬性,建議使用深拷貝
  • 深拷貝比淺拷貝要更加耗時,效率更低

為什么使用克隆

很重要并且常見的常見就是:某個API需要提供一個List集合,但是又不希望調用者的修改影響到自身的變化,因此需要克隆一份對象,以此達到數據隔離的目的。

應盡量避免clone

1.通常情況下,實現接口是為了表明類可以為它的客戶做些什么,而Cloneable僅僅是一個標記接口,而且還改變了超類中的手保護的方法的行為,是接口的一種極端非典型的用法,不值得效仿。

2.Clone方法約定及其脆弱 clone方法的Javadoc描述有點曖昧模糊,如下為 Java SE8的約定

clone方法創建并返回該對象的一個拷貝。而拷貝的精確含義取決于該對象的類。一般的含義是,對于任何對象x,表達式

x.clone() != x 為 true x.clone().getClass() == x.getClass() 也返回true,但非必須 x.clone().equals(x) 也返回true,但也不是必須的

上面的第二個和第三個表達式很容易就返回false。因而唯一能保證永久為true的就是表達式一,即兩個對象為獨立的對象。

3.可變對象final域 在克隆方法中,如果我們需要對可變對象的final域也進行拷貝,由于final的限制,所以實際上是無法編譯通過的。因此為了實現克隆,我們需要考慮舍去該可變對象域的final關鍵字。

4.線程安全 如果你決定用線程安全的類實現Cloneable接口,需要保證它的clone方法做好同步工作。默認的Object.clone方法是沒有做同步的。

總的來說,java中的clone方法實際上并不是完善的,建議盡量避免使用。如下是一些替代方案。

Copy constructors

使用復制構造器也可以實現對象的拷貝。

  • 復制構造器也是構造器的一種
  • 只接受一個參數,參數類型為當前的類
  • 目的是生成一個與參數相同的新對象

復制構造器相比clone方法的優勢是簡單,易于實現。一段使用了復制構造器的代碼示例

public class Car {
  Wheel wheel;
  String manufacturer;

  public Car(Wheel wheel, String manufacturer) {
      this.wheel = wheel;
      this.manufacturer = manufacturer;
  }

  //copy constructor
  public Car(Car car) {
      this(car.wheel, car.manufacturer);
  }

  public static class Wheel {
      String brand;
  }
}

注意,上面的代碼實現為淺拷貝,如果想要實現深拷貝,參考如下代碼

//copy constructor
public Car(Car car) {
  Wheel wheel = new Wheel();
  wheel.brand = car.wheel.brand;

  this.wheel = wheel;
  this.manufacturer = car.manufacturer;
}

為了更加便捷,我們還可以為上述類增加一個靜態的方法

public static Car newInstance(Car car) {
  return new Car(car);
}

使用Serializable實現深拷貝

其實,使用序列化也可以實現對象的深拷貝。簡略代碼如下

public class DeepCopyExample implements Serializable{
  private static final long serialVersionUID = 6098694917984051357L;
  public Child child;

  public DeepCopyExample copy() {
      DeepCopyExample copy = null;
      try {
          ByteArrayOutputStream baos = new ByteArrayOutputStream();
          ObjectOutputStream oos = new ObjectOutputStream(baos);
          oos.writeObject(this);

          ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
          ObjectInputStream ois = new ObjectInputStream(bais);
          copy = (DeepCopyExample) ois.readObject();
      } catch (IOException e) {
          e.printStackTrace();
      } catch (ClassNotFoundException e) {
          e.printStackTrace();
      }
      return copy;
  }
}

其中,Child必須實現Serializable接口

public class Child implements Serializable{
  private static final long serialVersionUID = 6832122780722711261L;
  public String name = "";

  public Child(String name) {
      this.name = name;
  }

  @Override
  public String toString() {
      return "Child [name=" + name + "]";
  }
}

使用示例兼測試代碼

DeepCopyExample example = new DeepCopyExample();
example.child = new Child("Example");

DeepCopyExample copy = example.copy();
if (copy != null) {
  copy.child.name = "Copied";
  System.out.println("example.child=" + example.child + ";copy.child=" + copy.child);
}
//輸出結果:example.child=Child [name=Example];copy.child=Child [name=Copied]

由輸出結果來看,copy對象的child值修改不影響example對象的child值,即使用序列化可以實現對象的深拷貝。

參考資料

推薦一本書

《Effective Java》 第11條即介紹謹慎使用clone。除此之外,本書還詳細介紹了很多關于Java細節的知識,是Java程序員很值得閱讀的一本書。本書也是經典的Jolt獲獎作品,作者是Joshua Bloch大神。是一本深入研究Java的參考書籍。

via: http://www.udpwork.com/item/15497.html

 本文由用戶 myhome1998 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!