從JDK角度看物件克隆

超人汪小建發表於2018-02-23

物件克隆

物件克隆其實是很常見的操作,它完成的功能是將現有物件內容(屬性)拷貝到新的物件中,得到的是一個新的物件,而並不只是一個物件引用。

其實對於屬性不多的物件我們可以直接通過編寫程式碼逐一屬性複製,比如我們可以直接 new 一個新物件,然後通過 set 方法將屬性值一個個設定進去。但這種做法我們也是比較不屑,看起來不夠高階,而且欄位一多就會造成程式碼冗長。另外,可能有些私有變數也無法這樣拷貝,所以克隆操作一般都使用 Java 內建的 Cloneable 介面實現。

簡單例子

下面是一個簡單的複製操作,對某個 Person 物件呼叫其 clone 方法即可以實現物件克隆。

public class Person implements Cloneable {

  public int age;

  public Person(int age) {
    this.age = age;
  }

  public Person clone() {
    Person o = null;
    try {
      o = (Person) super.clone();
    } catch (CloneNotSupportedException e) {
      e.printStackTrace();
    }
    return o;
  }
}
複製程式碼

淺拷貝

因為是物件導向程式設計,所以物件在克隆過程中就會涉及到淺拷貝和深拷貝的問題。每個物件被建立後基本都會被一個引用變數來表示,這個引用指向了物件的地址,而當使用 Object 物件的 clone 方法進行克隆時,它會對原始資料型別的值直接複製一份新值,而如果物件的屬性為引用型別時則會複製相應的引用值,所以此時複製的僅僅只是物件引用,克隆出來的物件的屬性和原來物件的屬性其實是指向同一個物件例項的。

直接通過下圖更好理解,Person 物件包含 age、name 和 birDate 屬性,name 為 String 型別的物件,而 birDate 為 Date 型別物件,那麼通過預設的克隆策略克隆出來後為右邊的 P_Copy 物件,name 和 birDate 屬性都是指向原來 Person 物件屬性指向的物件例項。

淺拷貝不是真的完全拷貝,它們可以各自修改自己的 age 屬性而不會影響到彼此,但如果改動了 name 或 birDate 引用物件的值將會互相影響。它的優點是能節省記憶體空間。

這裡寫圖片描述

public class Person implements Cloneable {

  public int age;
  private String name;
  private Date birDate;

  public Person(String name, int age) {
    this.age = age;
    this.name = name;
    this.birDate = new Date();
  }

  public Person clone() {
    Person o = null;
    try {
      o = (Person) super.clone();
    } catch (CloneNotSupportedException e) {
      e.printStackTrace();
    }
    return o;
  }
}
複製程式碼

深拷貝

與淺拷貝對應的為深拷貝,既然預設的克隆策略是不能實現完成拷貝的,即不能將原來物件中的屬性物件複製出一份新的副本。對於淺拷貝的節省記憶體空間,有時更需要的是克隆出完全互不影響的物件,這時就會用到深拷貝。

深拷貝的效果如下面的圖所示,與淺拷貝相比,這時除了 age 屬性外,name 和 birDate 屬性也都有了自己的副本,達到了深拷貝的效果。

深拷貝屬於真正的完全拷貝,它們可以各自修改自己的所有屬性而不會影響到彼此。它的缺點是會消耗記憶體空間。

這裡寫圖片描述

如下程式碼,要實現深拷貝就在 clone 方法中對需要拷貝的屬性物件進行額外克隆並且賦值給對應的屬性,這樣就能實現深拷貝。

public class Person implements Cloneable {

  public int age;
  private String name;
  private Date birDate;

  public Person(String name, int age) {
    this.age = age;
    this.name = name;
    this.birDate = new Date();
  }

  public Person clone() {
    Person o = null;
    try {
      o = (Person) super.clone();
      o.birDate = (Date) this.birDate.clone();
    } catch (CloneNotSupportedException e) {
      e.printStackTrace();
    }
    return o;
  }

}
複製程式碼

關於Cloneable介面

Cloneable介面沒有定義任何方法,那麼它有什麼用呢?其實它的作用是為了標明哪些物件可以實現拷貝,實現了該介面的物件才能通過 JVM 執行克隆操作時的檢查,沒有實現該介面的會被丟擲 CloneNotSupportedException 異常而無法進行克隆操作。

public interface Cloneable {
}
複製程式碼

另外,還約定實現了 Cloneable 介面的類需要重寫 Object 類的 clone 方法,重寫該方法最簡單的方式就是直接通過 super.clone() 呼叫 Object 的 clone方法。

Object的clone方法

Object的clone方法其實是一個本地方法,由本地方法表知道clone方法對應的本地函式為JVM_Clone,clone方法主要實現物件的克隆功能,根據該物件生成一個相同的新物件(我們常見的類的物件的屬性如果是原始型別則會克隆值,但如果是物件則會克隆物件的地址)。

protected native Object clone() throws CloneNotSupportedException;
複製程式碼

從程式碼中也解釋了為什麼需要實現Cloneable介面,if (!klass->is_cloneable())這裡會校驗是否有實現該介面,沒有實現的則會拋 CloneNotSupportedException 異常。然後判斷是否是陣列分兩種情況分配記憶體空間,新物件為new_obj,接著對new_obj進行copy及C++層資料結構的設定。最後再轉成jobject型別方便轉成Java層的Object型別。

JVM_ENTRY(jobject, JVM_Clone(JNIEnv* env, jobject handle))
  JVMWrapper("JVM_Clone");
  Handle obj(THREAD, JNIHandles::resolve_non_null(handle));
  const KlassHandle klass (THREAD, obj->klass());
  JvmtiVMObjectAllocEventCollector oam;

  if (!klass->is_cloneable()) {
    ResourceMark rm(THREAD);
    THROW_MSG_0(vmSymbols::java_lang_CloneNotSupportedException(), klass->external_name());
  }

  const int size = obj->size();
  oop new_obj = NULL;
  if (obj->is_javaArray()) {
    const int length = ((arrayOop)obj())->length();
    new_obj = CollectedHeap::array_allocate(klass, size, length, CHECK_NULL);
  } else {
    new_obj = CollectedHeap::obj_allocate(klass, size, CHECK_NULL);
  }
  Copy::conjoint_jlongs_atomic((jlong*)obj(), (jlong*)new_obj,
                               (size_t)align_object_size(size) / HeapWordsPerLong);
  new_obj->init_mark();

  BarrierSet* bs = Universe::heap()->barrier_set();
  assert(bs->has_write_region_opt(), "Barrier set does not have write_region");
  bs->write_region(MemRegion((HeapWord*)new_obj, size));

  if (klass->has_finalizer()) {
    assert(obj->is_instance(), "should be instanceOop");
    new_obj = instanceKlass::register_finalizer(instanceOop(new_obj), CHECK_NULL);
  }

  return JNIHandles::make_local(env, oop(new_obj));
JVM_END
複製程式碼

序列化方式克隆

除了上述的通過 clone 方法來克隆外,還有一種方式可以實現克隆操作,即是序列化方式,將物件先序列化為二進位制位元組流,然後通過這些位元組生成相同的屬性值的物件。比如通過如下方式克隆一個物件,name 和 birDate 屬性的引用與原來的物件屬性的引用是不同的,對它們引用的物件進行修改是不會影響到原來的物件的屬性的。

public class PersonSerialization implements Serializable {

  private static final long serialVersionUID = 4637638474632555808L;
  private String name;
  private int age;
  private Date birDate;

  public PersonSerialization(String name, int age) {
    this.name = name;
    this.age = age;
    this.birDate = new Date();
  }

  public PersonSerialization clone() {

    ByteArrayOutputStream byteOut = null;
    ObjectOutputStream objOut = null;
    ByteArrayInputStream byteIn = null;
    ObjectInputStream objIn = null;

    try {
      byteOut = new ByteArrayOutputStream();
      objOut = new ObjectOutputStream(byteOut);
      objOut.writeObject(this);
      byteIn = new ByteArrayInputStream(byteOut.toByteArray());
      objIn = new ObjectInputStream(byteIn);
      return (PersonSerialization) objIn.readObject();
    } catch (IOException | ClassNotFoundException e) {
      e.printStackTrace();
    } finally {
      try {
        byteIn = null;
        byteOut = null;
        if (objOut != null) objOut.close();
        if (objIn != null) objIn.close();
      } catch (IOException e) {}
    }
    return null;
  }
}
複製程式碼

-------------推薦閱讀------------

我的2017文章彙總——機器學習篇

我的2017文章彙總——Java及中介軟體

我的2017文章彙總——深度學習篇

我的2017文章彙總——JDK原始碼篇

我的2017文章彙總——自然語言處理篇

我的2017文章彙總——Java併發篇

------------------廣告時間----------------

公眾號的選單已分為“分散式”、“機器學習”、“深度學習”、“NLP”、“Java深度”、“Java併發核心”、“JDK原始碼”、“Tomcat核心”等,可能有一款適合你的胃口。

鄙人的新書《Tomcat核心設計剖析》已經在京東銷售了,有需要的朋友可以購買。感謝各位朋友。

為什麼寫《Tomcat核心設計剖析》

歡迎關注:

這裡寫圖片描述

相關文章