1.正確的克隆對象
當(dāng)需要拷貝一個對象時,很多人建議不使用Java本身的clone方法,理由之一是:正確的實(shí)現(xiàn)clone不太容易。的確如此,正確的實(shí)現(xiàn)對象的clone,有以下幾個步驟:
- 待Clone的對象需要實(shí)現(xiàn)
Cloneable接口。 - 覆蓋
Object的protected Object clone()方法為待Clone對象的public Object clone()方法。 - 待Clone對象及其子類的
clone()方法里需要調(diào)用super.clone()方法并處理CloneNotSupportedException異常。
一個clone()方法的正確實(shí)現(xiàn)如下所示:
class Room implements Cloneable{
private String name = "matrix";
private int price = 500;
public Object clone() {
try {
return super.clone();
} catch (CloneNotSupportedException e) {
// 由于實(shí)現(xiàn)了Cloneable接口,那么永不發(fā)生
}
return null;
}
}
2. clone存在的問題和原因
重新審視代碼,卻會發(fā)現(xiàn)一些奇怪的地方。
首先,接口Cloneable只是一個標(biāo)記接口,其中沒有任何方法,但是接口文檔表明,如果待Clone對象不實(shí)現(xiàn)該接口,就會拋出CloneNotSupportedException異常。解答該問題,需要深入JDK源碼Object的clone()方法,截取以下片段說明:
// Check if class of obj supports the Cloneable interface.
// All arrays are considered to be cloneable (See JLS 20.1.5)
// 檢查對象是否實(shí)現(xiàn)了Cloneable接口(數(shù)組默認(rèn)實(shí)現(xiàn)Cloneable)
if (!klass->is_cloneable()) {
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_oop = NULL;
// 分配空間
if (obj->is_javaArray()) {
const int length = ((arrayOop)obj())->length();
new_obj_oop = CollectedHeap::array_allocate(klass, size, length, CHECK_NULL);
} else {
new_obj_oop = CollectedHeap::obj_allocate(klass, size, CHECK_NULL);
}
// 具體的拷貝過程
Copy::conjoint_jlongs_atomic((jlong*)obj(), (jlong*)new_obj_oop,
(size_t)align_object_size(size) / HeapWordsPerLong);
clone()方法并沒有聲明在Cloneable中從而使用Java自有的接口語言特性實(shí)現(xiàn),而是在clone()方法的底層硬編碼建立和接口的聯(lián)系。沒有使用接口語言特性,這是clone()不好用的一大原因。
其次,Object類的clone()方法的訪問權(quán)限聲明為protected,而待Clone對象需要覆蓋聲明為public。一個不可考的原因是:Java在互聯(lián)網(wǎng)發(fā)展時期,遇到了某些安全性問題,一些對象并不希望能被克?。ū热缬脩舻拿艽a),由此,將Object中clone()方法的權(quán)限由public降低為protected,從而使對象默認(rèn)不具有Clone能力,以便提高安全性。
最后,需要在待Clone對象中約定調(diào)用super.clone()。原因正是要最終調(diào)用Object中的clone()方法,以便執(zhí)行具體的克隆過程。
3. 淺拷貝和深拷貝
明白了這些,感覺很開心,繼續(xù)擴(kuò)充代碼,在房子里開一扇窗:
class Window implements Cloneable{
private int width = 200;
private int height = 300;
public Object clone() {
try {
return super.clone();
} catch (CloneNotSupportedException e) {
// never happen
}
return null;
}
}
class Room implements Cloneable{
private String name = "matrix";
private int price = 12;
Window window = new Window();
// clone方法相同省略
}
愉快的克隆一間房子:
public static void main(String[] args) {
Room room = new Room();
Room clone = (Room) room.clone();
System.out.println(room != clone); // true
System.out.println(room.window != clone.window); // false
}
結(jié)果卻讓人失望,克隆出來的新房子和老房子共享了同一扇窗子,這并不是我們希望的。回顧先前clone()方法的native源碼,其中新對象中的字節(jié)由老對象拷貝而來,而Window window = new Window()在Room中存儲的是一個引用,所以拷貝的僅僅是一個引用。更官方的說法是:field by filed copy即按字段拷貝。也許你已經(jīng)聽說過,這種拷貝方式稱之為淺拷貝,是JAVA的默認(rèn)實(shí)現(xiàn)方式。與之對應(yīng)的另一種拷貝方式稱之為深拷貝,這種方式會將房子中的窗子也拷貝,所以需要額外的代碼實(shí)現(xiàn),由于窗子已經(jīng)實(shí)現(xiàn)Cloneable,所以僅需在Room中添加一行代碼:
public Object clone() {
try {
Room room = (Room) super.clone();
// 窗子也需要克隆
room.window = (Window) room.window.clone();
return room;
} catch (CloneNotSupportedException e) {
// never happen
}
return null;
}
再次克隆一間房子,運(yùn)行結(jié)果如下,終于不用擔(dān)心鄰居關(guān)閉自家的窗戶了。
true
true
4.clone的精確含義
骨傲天是個我行我素的人,憑什么要遵守約定調(diào)用super.clone()呢?于是他使用魔法準(zhǔn)備克隆一間教室:
// 普通房間改造的教室,里面空空如也
class ClassRoom extends Room {
}
class Room implements Cloneable{
private String name = "matrix";
private int price = 12;
Window window = new Window();
public Object clone() {
Room room = new Room();
room.window = new Window();
return room;
}
}
克隆開始:
public static void main(String[] args) {
Room classRoom = new ClassRoom();
Room cloneClass = (Room) classRoom.clone();
System.out.println(classRoom != cloneClass);
System.out.println(classRoom.window != cloneClass.window);
System.out.println(classRoom.getClass());
System.out.println(cloneClass.getClass());
}
克隆的結(jié)果:
true
true
class clone.ClassRoom
class clone.Room
開始地很高興,結(jié)束地很傷心,克隆出的根本不是教室,而是老房子。這不是一次成功的克隆,違背了克隆的定義。而JAVA克隆的精確定義需要滿足以下三個條件:
-
x.clone() != x必為真 - 一般情況,
x.clone().getClass() == x.getClass()為真 - 一般情況,
x.clone().equals(x)為真
如果不遵守約定調(diào)用super.clone(),那么將會違背第二個條件,使得克隆出的對象與原對象不屬于同一個類型。
5.其他的解決方案
由于JAVA的clone()方法在深拷貝方面有諸多缺陷,涌現(xiàn)出了許多解決方案:
- Copy Constructor即提供一個可拷貝對象的構(gòu)造方法。比如在
Window中提供一個如下的構(gòu)造方法:
public Window(Window window) {
this.width = window.width;
this.height = window.height;
}
- 序列化一個對象之后再反序列化。比如先將對象轉(zhuǎn)換為JSON字符串,然后在反序列化得到新對象。Kryo的序列化機(jī)制克隆速度更快,可以參考Kryo。
- 使用反射逐字段克隆對象。如Java Deep Cloning Library。
如果一個對象中只包含基本數(shù)據(jù)類型和不可變對象的引用,此種情況 下,深拷貝和淺拷貝的結(jié)果一致,那么推薦使用JAVA的clone()解決方案。
附一些關(guān)于clone的討論: