(二十三)原型模式詳解(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
您的支援是對博主最大的鼓勵,感謝您的認真閱讀。
本文版權歸作者所有,歡迎轉載,但未經作者同意必須保留此段宣告,且在文章頁面明顯位置給出原文連線,否則保留追究法律責任的權利。