作者: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]
com.prototype.Field@de6ced
[2,3,4,5]
com.prototype.Field@de6ced
可以看到我們對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]
com.prototype.Field@a90653
[2,3,4,5]
com.prototype.Field@de6ced
下面我們來看下原型模式的主要優點:
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會寫一些虛擬機以及虛擬機源碼的相關內容,希望各位能繼續支持吧。
感謝各位的收看。