Java设计模式百例(番外) - Java的clone
本文源碼見:https://github.com/get-set/get-designpatterns/tree/master/prototype
本文是為下一篇“Java設計模式百例 - 原型模式”做鋪墊,討論一下Java中的對象克隆。本文內容綜合了《Effective Java》、《Java與模式》以及其他網上相關資料,希望能夠對您也有所幫助。
Java中,對象的創建除了用new關鍵字,還可以使用既有對象的clone()方法來復制自身達到創建一個新對象的目的。
關于對象克隆,Java中有通用約定:
通用約定1: x.clone() != x 必須為真。
對象克隆與引用的復制是有本質區別的,區別就在于x.clone()后產生的對象與x并不位于同一塊內存上,兩者是獨立的,修改兩者任何一方的成員都不會導致另一方發生變化。就像克隆羊多利(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@Overridepublic?Sheep?clone()?throws?CloneNotSupportedException?{return?(Sheep)?super.clone();}@Overridepublic?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接口,否則執行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了一個對象,因此可以認為復制了引用指向的對象。其效果”看起來“和原生類型的待遇是一樣的。
總結來說,被復制對象的所有原生類型變量和不可變類的引用都復制與原來的對象相同的值,而所有的對其他對象(不包含不可變類的對象)的引用仍然指向原來的對象。
深復制
相對于淺復制,更進一步,深拷貝把要復制的對象所引用的對象都復制一遍。
實現深復制有兩種方式。一種是繼續利用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()方法呢,如果是一個引用鏈,深度復制要達到什么樣的深度呢?是否有循環引用呢(比如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;@Overridepublic?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
????@Overridepublic?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
????@Overridepublic?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)若要深克隆對象,則需要增加對引用為非不可變對象的克隆。
轉載于:https://blog.51cto.com/liukang/1982920
總結
以上是生活随笔為你收集整理的Java设计模式百例(番外) - Java的clone的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Eclipse中最常用的热键
- 下一篇: Androidi性能优化之多线程和同步