本文源碼見:https://github.com/get-set/get-designpatterns/tree/master/prototype
本文是為下一篇“Java設(shè)計(jì)模式百例 - 原型模式”做鋪墊,討論一下Java中的對象克隆。本文內(nèi)容綜合了《Effective Java》、《Java與模式》以及其他網(wǎng)上相關(guān)資料,希望能夠?qū)δ灿兴鶐椭?/p>
Java中,對象的創(chuàng)建除了用new
關(guān)鍵字,還可以使用既有對象的clone()
方法來復(fù)制自身達(dá)到創(chuàng)建一個(gè)新對象的目的。
關(guān)于對象克隆,Java中有通用約定:
通用約定1: x.clone() != x 必須為真。
對象克隆與引用的復(fù)制是有本質(zhì)區(qū)別的,區(qū)別就在于x.clone()后產(chǎn)生的對象與x并不位于同一塊內(nèi)存上,兩者是獨(dú)立的,修改兩者任何一方的成員都不會(huì)導(dǎo)致另一方發(fā)生變化。就像克隆羊多利(Dolly)不會(huì)因?yàn)槠洹盎蚰赣H”(很遺憾,它沒有名字,我們暫且諧音基因,就叫Jane吧)受傷或死亡而受傷或死亡。代碼舉例:
Sheep.java
public class Sheep implements Cloneable {
private String name; //名字
private int age; //年齡
private String breed; //品種
private EarTag earTag; //耳牌
// 構(gòu)造方法
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() + "號。";
}
}
每只羊身上有個(gè)耳牌:
EarTag.java
public class EarTag implements Cloneable {
private int id; //耳牌編號
private String color; //耳牌顏色
// 構(gòu)造方法
public EarTag(int id, String color) {
this.id = id;
this.color = color;
}
// getters & setters
}
注意,
- 以上兩個(gè)類均需要實(shí)現(xiàn)
Cloneable
接口,否則執(zhí)行clone()
方法會(huì)報(bào)CloneNotSupportedException
異常。 - 若某個(gè)類允許其對象可以克隆,那么需要重寫
clone()
方法,并且聲明為public
的,因?yàn)?code>Object的clone()
方法是protected
,無法被非子類和不在當(dāng)前包的其他類或?qū)ο笳{(diào)用。 - 派生類的
clone()
方法中,要調(diào)用super.clone()
,以便能夠最終調(diào)用到Object.clone()
,后者是個(gè)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);
輸出結(jié)果為:
簡是一只5歲的多塞特白面綿羊, 它的黃色色耳牌上寫著12345號。
克隆后...
多利是一只5歲的多塞特白面綿羊, 它的黃色色耳牌上寫著12346號。
仿佛很完美,所有的信息都克隆過來了,但是,我們在看一下jane
這個(gè)對象(最后增加兩個(gè)輸出):
System.out.println(jane);
System.out.println(jane.getEarTag() == dolly.getEarTag());
輸出結(jié)果為:
簡是一只5歲的多塞特白面綿羊, 它的黃色色耳牌上寫著12346號。
true
這就不對了,簡的耳牌號也變了,而且我們看到兩只羊的耳牌是”==“的,也就是jane.earTag
和dolly.earTag
指向的是同一個(gè)對象。這在現(xiàn)實(shí)中是毫無道理的。可見,earTag
這個(gè)成員變量是引用復(fù)制。
淺克隆
上邊例子中,最終調(diào)用到的Object.clone()
就是淺克隆。所謂淺克隆,可以理解為只復(fù)制成員變量的”值“。
- 對于原生類型,其”值“就是實(shí)實(shí)在在的值,比如
int age
,是直接復(fù)制的; - 對于引用類型,其”值“就是引用本身,比如
EarTag earTag
,引用原來指向的是”黃色編號為12345的牌子“,引用復(fù)制過來仍然是指向同樣的牌子,所以只是復(fù)制的值,而并未復(fù)制引用指向的對象; - (補(bǔ)充)對于引用類型,如果引用本身指向的是不可變類,比如
String
、Integer
等,引用指向的對象內(nèi)容是不可變的,一旦需要改變,其實(shí)就是從新new
了一個(gè)對象,因此可以認(rèn)為復(fù)制了引用指向的對象。其效果”看起來“和原生類型的待遇是一樣的。
總結(jié)來說,被復(fù)制對象的所有原生類型變量和不可變類的引用都復(fù)制與原來的對象相同的值,而所有的對其他對象(不包含不可變類的對象)的引用仍然指向原來的對象。
深克隆
相對于淺克隆,更進(jìn)一步,深克隆把要復(fù)制的對象所引用的對象都復(fù)制一遍。
實(shí)現(xiàn)深克隆有兩種方式。一種是繼續(xù)利用clone()方法,另一種是利用對象序列化。
對于第一種方法,進(jìn)一步手動(dòng)將指向可變對象的引用再復(fù)制一遍即可。比如對于Sheep
我們增加deepClone()
方法,在該方法中明確將EarTag
對象也復(fù)制一下。因此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()
方法,別忘了實(shí)現(xiàn)Cloneable
接口
@Override
public EarTag clone() throws CloneNotSupportedException {
return (EarTag) super.clone();
}
這時(shí)候再測試一遍看輸出:
簡是一只5歲的多塞特白面綿羊, 它的黃色色耳牌上寫著12345號。
克隆后...
多利是一只6歲的多塞特白面綿羊, 它的黃色色耳牌上寫著12346號。
簡是一只5歲的多塞特白面綿羊, 它的黃色色耳牌上寫著12345號。
false
可見,EarTag對象也被克隆了。
這時(shí),其實(shí)還需要注意一個(gè)問題,我們這個(gè)例子中,EarTag
的對象沒有指向其他對象的引用,假設(shè)有的話,是否要調(diào)用EarTag
的deepClone()
方法呢,如果是一個(gè)引用鏈,深度復(fù)制要達(dá)到什么樣的深度呢?是否有循環(huán)引用呢(比如EarTag
中又有對Sheep
的引用)?這都是在具體的使用過程中需要謹(jǐn)慎考慮的。
第二種方法是通過對象序列化來實(shí)現(xiàn)對象的深克隆。在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
都需要實(shí)現(xiàn)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
可見也確實(shí)實(shí)現(xiàn)了深克隆。
通用約定2: x.clone().getClass() == x.getClass() 必須為真。
指的是克隆后的對象其類型是一致的。這一點(diǎn)沒有問題,及時(shí)在有繼承關(guān)系的情況下。
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());
結(jié)果為:
b
可見,即使子類沒有重寫clone()
方法,只要其各層父類中有重新了public
的clone()
方法的,那么clone()
方法都能正確克隆調(diào)起該方法的對象,且類型正確。話說回來,畢竟clone()
的動(dòng)作最終都是源于Object
的那個(gè)native方法的。
通用約定3: x.clone().equals(x)為真
這一條并非強(qiáng)制約束,但盡量保證做到。因?yàn)閺囊话阏J(rèn)識上來講,克隆的兩個(gè)對象雖然是不相等(==)的,但應(yīng)該是相同(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
,表示兩個(gè)對象是相同的。
總結(jié)
最后,我們總結(jié)一下,實(shí)現(xiàn)clone的方法:
1)在派生類中實(shí)現(xiàn)Cloneable借口;
2)在派生類中覆蓋基類的clone方法,聲明為public;
3)在派生類的clone方法中,調(diào)用super.clone();
4)若要深克隆對象,則需要增加對引用為非不可變對象的克隆。