本文源碼見:https://github.com/get-set/get-designpatterns/tree/master/prototype
本文是為下一篇“Java設計模式百例 - 原型模式”做鋪墊刀疙,討論一下Java中的對象克隆颊亮。本文內容綜合了《Effective Java》也切、《Java與模式》以及其他網上相關資料,希望能夠對您也有所幫助桶略。
Java中,對象的創(chuàng)建除了用new
關鍵字,還可以使用既有對象的clone()
方法來復制自身達到創(chuàng)建一個新對象的目的。
關于對象克隆密浑,Java中有通用約定:
通用約定1: x.clone() != x 必須為真蛙婴。
對象克隆與引用的復制是有本質區(qū)別的粗井,區(qū)別就在于x.clone()后產生的對象與x并不位于同一塊內存上,兩者是獨立的街图,修改兩者任何一方的成員都不會導致另一方發(fā)生變化浇衬。就像克隆羊多利(Dolly)不會因為其“基因母親”(很遺憾,它沒有名字餐济,我們暫且諧音基因耘擂,就叫Jane吧)受傷或死亡而受傷或死亡。代碼舉例:
Sheep.java
public class Sheep implements Cloneable {
private String name; //名字
private int age; //年齡
private String breed; //品種
private EarTag earTag; //耳牌
// 構造方法
public Sheep(String name, int age, String breed, EarTag earTag) {
this.name = name;
this.age = age;
this.breed = breed;
this.earTag = earTag;
}
// getters & setters
@Override
public Sheep clone() throws CloneNotSupportedException {
return (Sheep) super.clone();
}
@Override
public String toString() {
return this.name + "是一只" + this.age + "歲的" + this.breed + ", 它的" + this.earTag.getColor() + "色耳牌上寫著" + this.earTag.getId() + "號絮姆。";
}
}
每只羊身上有個耳牌:
EarTag.java
public class EarTag implements Cloneable {
private int id; //耳牌編號
private String color; //耳牌顏色
// 構造方法
public EarTag(int id, String color) {
this.id = id;
this.color = color;
}
// getters & setters
}
注意醉冤,
- 以上兩個類均需要實現
Cloneable
接口,否則執(zhí)行clone()
方法會報CloneNotSupportedException
異常篙悯。 - 若某個類允許其對象可以克隆蚁阳,那么需要重寫
clone()
方法,并且聲明為public
的鸽照,因為Object
的clone()
方法是protected
螺捐,無法被非子類和不在當前包的其他類或對象調用。 - 派生類的
clone()
方法中矮燎,要調用super.clone()
定血,以便能夠最終調用到Object.clone()
,后者是個native方法诞外,效率更高澜沟。
克隆過程如下:
Sheep jane = new Sheep("簡", 5, "多塞特白面綿羊", new EarTag(12345, "黃色"));
System.out.println(jane);
Sheep dolly = jane.clone();
System.out.println("克隆后...");
dolly.setName("多利");
dolly.getEarTag().setId(12346);
System.out.println(dolly);
輸出結果為:
簡是一只5歲的多塞特白面綿羊, 它的黃色色耳牌上寫著12345號。
克隆后...
多利是一只5歲的多塞特白面綿羊, 它的黃色色耳牌上寫著12346號峡谊。
仿佛很完美茫虽,所有的信息都克隆過來了,但是靖苇,我們在看一下jane
這個對象(最后增加兩個輸出):
System.out.println(jane);
System.out.println(jane.getEarTag() == dolly.getEarTag());
輸出結果為:
簡是一只5歲的多塞特白面綿羊, 它的黃色色耳牌上寫著12346號席噩。
true
這就不對了,簡的耳牌號也變了贤壁,而且我們看到兩只羊的耳牌是”==“的悼枢,也就是jane.earTag
和dolly.earTag
指向的是同一個對象。這在現實中是毫無道理的脾拆÷鳎可見莹妒,earTag
這個成員變量是引用復制。
淺克隆
上邊例子中绰上,最終調用到的Object.clone()
就是淺克隆旨怠。所謂淺克隆,可以理解為只復制成員變量的”值“蜈块。
- 對于原生類型鉴腻,其”值“就是實實在在的值,比如
int age
百揭,是直接復制的爽哎; - 對于引用類型,其”值“就是引用本身器一,比如
EarTag earTag
课锌,引用原來指向的是”黃色編號為12345的牌子“,引用復制過來仍然是指向同樣的牌子祈秕,所以只是復制的值渺贤,而并未復制引用指向的對象; - (補充)對于引用類型请毛,如果引用本身指向的是不可變類志鞍,比如
String
、Integer
等获印,引用指向的對象內容是不可變的述雾,一旦需要改變,其實就是從新new
了一個對象兼丰,因此可以認為復制了引用指向的對象玻孟。其效果”看起來“和原生類型的待遇是一樣的。
總結來說鳍征,被復制對象的所有原生類型變量和不可變類的引用都復制與原來的對象相同的值黍翎,而所有的對其他對象(不包含不可變類的對象)的引用仍然指向原來的對象。
深克隆
相對于淺克隆艳丛,更進一步匣掸,深克隆把要復制的對象所引用的對象都復制一遍。
實現深克隆有兩種方式氮双。一種是繼續(xù)利用clone()方法碰酝,另一種是利用對象序列化。
對于第一種方法戴差,進一步手動將指向可變對象的引用再復制一遍即可送爸。比如對于Sheep
我們增加deepClone()
方法,在該方法中明確將EarTag
對象也復制一下。因此EarTag
也需要重寫clone()
方法袭厂。
Sheep.java增加deepClone()
方法
public Sheep deepClone() throws CloneNotSupportedException {
Sheep s = (Sheep)super.clone();
s.setEarTag(s.getEarTag().clone());
return s;
}
EarTag.java增加clone()
方法墨吓,別忘了實現Cloneable
接口
@Override
public EarTag clone() throws CloneNotSupportedException {
return (EarTag) super.clone();
}
這時候再測試一遍看輸出:
簡是一只5歲的多塞特白面綿羊, 它的黃色色耳牌上寫著12345號。
克隆后...
多利是一只6歲的多塞特白面綿羊, 它的黃色色耳牌上寫著12346號纹磺。
簡是一只5歲的多塞特白面綿羊, 它的黃色色耳牌上寫著12345號帖烘。
false
可見,EarTag對象也被克隆了橄杨。
這時秘症,其實還需要注意一個問題,我們這個例子中讥珍,EarTag
的對象沒有指向其他對象的引用历极,假設有的話,是否要調用EarTag
的deepClone()
方法呢衷佃,如果是一個引用鏈,深度復制要達到什么樣的深度呢蹄葱?是否有循環(huán)引用呢(比如EarTag
中又有對Sheep
的引用)氏义?這都是在具體的使用過程中需要謹慎考慮的。
第二種方法是通過對象序列化來實現對象的深克隆图云。在Sheep.java中增加如下方法:
public Sheep serializedClone() throws IOException, ClassNotFoundException {
ByteArrayOutputStream bao = new ByteArrayOutputStream();
ObjectOutputStream oo = new ObjectOutputStream(bao);
oo.writeObject(this);
ByteArrayInputStream bai = new ByteArrayInputStream(bao.toByteArray());
ObjectInputStream oi = new ObjectInputStream(bai);
return (Sheep) oi.readObject();
}
注意的是惯悠,Sheep
和EarTag
都需要實現Serializable
接口棒厘,以便打開對序列化的支持休傍。
測試一下:
Sheep jane = new Sheep("簡", 5, "多塞特白面綿羊", new EarTag(12345, "黃色"));
System.out.println(jane);
Sheep dolly = jane.serializedClone();
System.out.println("克隆后...");
dolly.setName("多利");
dolly.setAge(6);
dolly.getEarTag().setId(12346);
System.out.println(dolly);
System.out.println(jane);
System.out.println(jane.getEarTag() == dolly.getEarTag());
輸出如下:
簡是一只5歲的多塞特白面綿羊, 它的黃色色耳牌上寫著12345號。
克隆后...
多利是一只6歲的多塞特白面綿羊, 它的黃色色耳牌上寫著12346號术吗。
簡是一只5歲的多塞特白面綿羊, 它的黃色色耳牌上寫著12345號丹泉。
false
可見也確實實現了深克隆情萤。
通用約定2: x.clone().getClass() == x.getClass() 必須為真。
指的是克隆后的對象其類型是一致的摹恨。這一點沒有問題筋岛,及時在有繼承關系的情況下。
ClassA.java
public class ClassA implements Cloneable {
public int getA() {
return a;
}
public void setA(int a) {
this.a = a;
}
private int a;
@Override
public ClassA clone() throws CloneNotSupportedException {
return (ClassA) super.clone();
}
}
ClassB.java(繼承ClassA)
public class ClassB extends ClassA {
private String b;
public String getB() {
return b;
}
public void setB(String b) {
this.b = b;
}
public void test() {
System.out.println(super.getClass().getCanonicalName());
}
}
測試一下:
ClassB b = new ClassB();
b.setA(1);
b.setB("b");
ClassB b1 = (ClassB) b.clone();
System.out.println(b1.getB());
結果為:
b
可見晒哄,即使子類沒有重寫clone()
方法睁宰,只要其各層父類中有重新了public
的clone()
方法的,那么clone()
方法都能正確克隆調起該方法的對象寝凌,且類型正確柒傻。話說回來,畢竟clone()
的動作最終都是源于Object
的那個native方法的较木。
通用約定3: x.clone().equals(x)為真
這一條并非強制約束红符,但盡量保證做到。因為從一般認識上來講,克隆的兩個對象雖然是不相等(==)的违孝,但應該是相同(equal)的刹前。
重寫Sheep.java和EarTag.java的equals()
方法:
Sheep.java
@Override
public boolean equals(Object obj) {
if (obj == this)
return true;
if (!(obj instanceof Sheep))
return false;
Sheep s = (Sheep) obj;
return s.name.equals(this.name) &&
s.age == this.age &&
s.breed.equals(this.breed) &&
s.earTag.equals(this.earTag);
}
EarTag.java
@Override
public boolean equals(Object obj) {
if (obj == this)
return true;
if (!(obj instanceof Sheep))
return false;
Sheep s = (Sheep) obj;
return s.name.equals(this.name) &&
s.age == this.age &&
s.breed.equals(this.breed) &&
s.earTag.equals(this.earTag);
}
測試一下:
Sheep jane = new Sheep("簡", 5, "多塞特白面綿羊", new EarTag(12345, "黃色"));
Sheep dolly = jane.serializedClone();
System.out.println("克隆后...");
System.out.println(jane.equals(dolly));
輸出為true
,表示兩個對象是相同的雌桑。
總結
最后喇喉,我們總結一下,實現clone的方法:
1)在派生類中實現Cloneable借口校坑;
2)在派生類中覆蓋基類的clone方法拣技,聲明為public;
3)在派生類的clone方法中耍目,調用super.clone()膏斤;
4)若要深克隆對象,則需要增加對引用為非不可變對象的克隆邪驮。