1. 程式人生 > >(二十三)原型模式詳解(clone方法原始碼的簡單剖析)

(二十三)原型模式詳解(clone方法原始碼的簡單剖析)

 作者:zuoxiaolong8810(左瀟龍),轉載請註明出處,特別說明:本博文來自博主原部落格,為保證新部落格中博文的完整性,特複製到此留存,如需轉載請註明新部落格地址即可。

                原型模式算是JAVA中最簡單的設計模式了,原因是因為它已經被提供了語言級的支援,但是如果提到它的實現原理,又是最複雜的一個設計模式。

                下面我們先來看看這個又簡單又複雜的設計模式的定義。

                定義:用原型例項指定建立物件的種類,並且通過拷貝這些原型建立新的物件。

                定義比較簡單,總結一下是通過例項指定種類,通過拷貝建立物件。

                在JAVA語言中使用原型模式是非常簡單的,這是因為Object類當中提供了一個本地方法clone,而JAVA中的任何類只要實現了Cloneable標識介面,就可以使用clone方法來進行物件的拷貝。

                我們寫一個簡單的例項來測試一下,很簡單。

複製程式碼

package com.prototype;

public class Prototype implements Cloneable {

    private int x;
    private int y;
    private int z;

    public Prototype() {
        this.x = 2;
        this.y = 3;
        this.z = 4;
    }

    public void change() {
        this.x = 9;
        this.y = 8;
        this.z = 7;
    }

    public Prototype clone() {
        Object object = null;
        try {
            object = super.clone();
        } catch (CloneNotSupportedException exception) {
            throw new RuntimeException(exception);
        }
        return (Prototype) object;
    }

    public String toString() {
        return "[" + x + "," + y + "," + z + "]";
    }

    public static void main(String[] args) {
        Prototype prototype1 = new Prototype();
        prototype1.change();
        System.out.println(prototype1);
        Prototype prototype2 = prototype1.clone();
        System.out.println(prototype2);
    }

}

複製程式碼

輸入結果:

[9,8,7]
[9,8,7]

                 

               從輸出結果可以看出來,clone方法將prototype1複製了一個,然後賦給了prototype2,這就像複製貼上一樣。值得注意的是,在使用Object.clone()方法去拷貝一個物件時,構造方法是不被執行的,否則prototype2例項中x,y,z的值應該為2,3,4才對,如果你覺得不夠直觀,可以在構造方法裡寫一個輸出語句試試。

               從原型模式的使用方式不難推斷出,原型模式常使用於以下場景:

               1、物件的建立非常複雜,可以使用原型模式快捷的建立物件。

               2、在執行過程中不知道物件的具體型別,可使用原型模式建立一個相同型別的物件,或者在執行過程中動態的獲取到一個物件的狀態。

               

               對於clone方法,它執行的是淺拷貝,也就是說如果是引用型別的屬性,則它不會進行拷貝,而是隻拷貝引用。

               看下面這個簡單的測試,就能看出來了。

複製程式碼

package com.prototype;

class Field{
    
    private int a;

    public int getA() {
        return a;
    }

    public void setA(int a) {
        this.a = a;
    }
    
}

public class ShallowPrototype implements Cloneable {

    private int x;
    private int y;
    private int z;
    private Field field;

    public ShallowPrototype() {
        this.x = 2;
        this.y = 3;
        this.z = 4;
        this.field = new Field();
        this.field.setA(5);
    }
    
    public Field getField() {
        return field;
    }

    public ShallowPrototype clone() {
        Object object = null;
        try {
            object = super.clone();
        } catch (CloneNotSupportedException exception) {
            throw new RuntimeException(exception);
        }
        return (ShallowPrototype) object;
    }

    public String toString() {
        return "[" + x + "," + y + "," + z + "," + field.getA() + "]";
    }

    public static void main(String[] args) {
        ShallowPrototype prototype1 = new ShallowPrototype();
        System.out.println(prototype1);
        System.out.println(prototype1.getField());
        ShallowPrototype prototype2 = prototype1.clone();
        System.out.println(prototype2);
        System.out.println(prototype2.getField());
    }

}

複製程式碼

輸入結果:

 

[2,3,4,5]
[email protected]
[2,3,4,5]
[email protected]

          

               可以看到我們對ShallowPrototype拷貝以後,得到一個例項prototype2,不過當我們輸出field屬性時,發現它們是引用的同一個物件。這當然不是我們期望得到的結果,這種情況下,我們如果修改prototype1中field的屬性a的值,則prototype2中的也會跟著改變。

               然而如果要實現深度拷貝,則需要將實現了Cloneable介面並重寫了clone方法的類中,所有的引用型別也全部實現Cloneable介面並重寫clone方法,而且需要將引用型別的屬性全部拷貝一遍。

               下面是一個簡單的深度拷貝的例子,由上面的例子更改得到。

複製程式碼

package com.prototype;

class Field implements Cloneable{
    
    private int a;

    public int getA() {
        return a;
    }

    public void setA(int a) {
        this.a = a;
    }
    
    protected Field clone() {
        Object object = null;
        try {
            object = super.clone();
        } catch (CloneNotSupportedException exception) {
            throw new RuntimeException(exception);
        }
        return (Field) object;
    }
    
}

public class DeepPrototype implements Cloneable {

    private int x;
    private int y;
    private int z;
    private Field field;

    public DeepPrototype() {
        this.x = 2;
        this.y = 3;
        this.z = 4;
        this.field = new Field();
        this.field.setA(5);
    }
    
    public Field getField() {
        return field;
    }

    protected DeepPrototype clone() {
        Object object = null;
        try {
            object = super.clone();
            ((DeepPrototype)object).field = this.field.clone();
        } catch (CloneNotSupportedException exception) {
            throw new RuntimeException(exception);
        }
        return (DeepPrototype) object;
    }

    public String toString() {
        return "[" + x + "," + y + "," + z + "," + field.getA() + "]";
    }

    public static void main(String[] args) {
        DeepPrototype prototype1 = new DeepPrototype();
        System.out.println(prototype1);
        System.out.println(prototype1.getField());
        DeepPrototype prototype2 = prototype1.clone();
        System.out.println(prototype2);
        System.out.println(prototype2.getField());
    }

}

複製程式碼

輸出結果:

 

[2,3,4,5]
[email protected]
[2,3,4,5]
[email protected]

 

               下面我們來看下原型模式的主要優點:

               1、由於clone方法是由虛擬機器直接複製記憶體塊執行,所以在速度上比使用new的方式建立物件要快。

               2、可以基於原型,快速的建立一個物件,而無需知道建立的細節。
               3、可以在執行時動態的獲取物件的型別以及狀態,從而建立一個物件。

               然而原型模式的缺點也是相當明顯的,主要的缺點就是實現深度拷貝比較困難,需要很多額外的程式碼量

               不過實際當中我們使用原型模式時,也可以寫一個基類實現Cloneable介面重寫clone方法,然後讓需要具有拷貝功能的子類繼承自該類,這是一種節省程式碼量的常用方式。像上面的例子一樣,如果一個類繼承自Prototype,則會自動具有拷貝功能。

               

               下面我們來看看虛擬機器中本地方法Object.clone()的原始碼,如下。

複製程式碼

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;

#ifdef ASSERT
  // Just checking that the cloneable flag is set correct
  if (obj->is_javaArray()) {
    guarantee(klass->is_cloneable(), "all arrays are cloneable");
  } else {
    guarantee(obj->is_instance(), "should be instanceOop");
    bool cloneable = klass->is_subtype_of(SystemDictionary::Cloneable_klass());
    guarantee(cloneable == klass->is_cloneable(), "incorrect cloneable flag");
  }
#endif

  // Check if class of obj supports the Cloneable interface.
  // All arrays are considered to be cloneable (See JLS 20.1.5)
  if (!klass->is_cloneable()) {//這裡檢查了是否實現了Cloneable介面,如果沒實現,會丟擲異常CloneNotSupportException。
    ResourceMark rm(THREAD);
    THROW_MSG_0(vmSymbols::java_lang_CloneNotSupportedException(), klass->external_name());
  }

  // Make shallow object copy
  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);//分配記憶體,寫入元資料資訊
  }
  // 4839641 (4840070): We must do an oop-atomic copy, because if another thread
  // is modifying a reference field in the clonee, a non-oop-atomic copy might
  // be suspended in the middle of copying the pointer and end up with parts
  // of two different pointers in the field.  Subsequent dereferences will crash.
  // 4846409: an oop-copy of objects with long or double fields or arrays of same
  // won't copy the longs/doubles atomically in 32-bit vm's, so we copy jlongs instead
  // of oops.  We know objects are aligned on a minimum of an jlong boundary.
  // The same is true of StubRoutines::object_copy and the various oop_copy
  // variants, and of the code generated by the inline_native_clone intrinsic.
  assert(MinObjAlignmentInBytes >= BytesPerLong, "objects misaligned");
  Copy::conjoint_jlongs_atomic((jlong*)obj(), (jlong*)new_obj,
                               (size_t)align_object_size(size) / HeapWordsPerLong);//這一步就是真正的COPY記憶體塊了
  // Clear the header
  new_obj->init_mark();//初始化物件頭,裡面包含了Hashcode,GC資訊,鎖資訊等,因為拷貝出的物件是一個全新的物件,所以這些資訊需要初始化一下。

  // Store check (mark entire object and let gc sort it out)
  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));//write_region最終的實現在一個虛方法裡,相當於JAVA的抽象方法,LZ沒找到實現。暫不發表意見。

  // Caution: this involves a java upcall, so the clone should be
  // "gc-robust" by this stage.
  if (klass->has_finalizer()) {//如果有finalize方法,則需要註冊一下。
    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));//將記憶體物件轉換成JAVA本地物件返回
JVM_END

複製程式碼

               虛擬機器的原始碼比較複雜,而且完全沒有相關文獻和資料,所以LZ也只能簡單的新增一些註釋,特別是write_region這個方法,LZ沒找到實現在哪裡,LZ猜測這個方法的功能是設定物件的邊界的,好讓GC能夠正確的回收記憶體,但由於沒找到實現,所以不敢斷言。

               在上面的過程中呼叫了Copy物件的conjoint_jlongs_atomic方法,那個就是真正的複製例項資料的方法,LZ找到了這個方法的實現,給各位看一下。

複製程式碼

void _Copy_conjoint_jlongs_atomic(jlong* from, jlong* to, size_t count) {
    if (from > to) {
      jlong *end = from + count;
      while (from < end)
        os::atomic_copy64(from++, to++);
    }
    else if (from < to) {
      jlong *end = from;
      from += count - 1;
      to   += count - 1;
      while (from >= end)
        os::atomic_copy64(from--, to--);
    }
  }

複製程式碼

              這是一個操作記憶體塊的方法,其中atomic_copy64這個方法是用匯編語言寫的,它確保了在64位的機子下也可以正確的進行記憶體塊的拷貝操作。它的作用很簡單,就是把from指標指向的記憶體的值賦給to指標指向的記憶體,也就是一個簡單的拷貝操作。知道了atomic_copy64方法的作用,上面這個方法的邏輯就非常簡單了。

              由此可以看出,我們可以將clone方法想象成記憶體塊的複製操作,它的速度比一般的建立物件操作要快

              

              原型模式的分析就到此結束了,對於虛擬機器原始碼的研究,LZ一直在斷斷續續的繼續著,等設計模式系列寫完以後,LZ會寫一些虛擬機器以及虛擬機器原始碼的相關內容,希望各位能繼續支援吧。

              感謝各位的收看。
 

 

               

 

版權宣告

 


作者:zuoxiaolong(左瀟龍)

出處:部落格園左瀟龍的技術部落格--http://www.cnblogs.com/zuoxiaolong

您的支援是對博主最大的鼓勵,感謝您的認真閱讀。

本文版權歸作者所有,歡迎轉載,但未經作者同意必須保留此段宣告,且在文章頁面明顯位置給出原文連線,否則保留追究法律責任的權利。