I walk very slowly, but I never walk backwards
設計模式 - 原型模式(下)
寂然
大家好~,我是寂然,本節課呢,我們對原型模式進行深入一點的讨論,我們來聊聊深拷貝和淺拷貝
前情提要
上節課,我們聊了聊小李求職的事情,寫了一個關于原型模式的案例,大家有沒有發現,我們使用原型模式建立類 Resume 的對象時,類 Resume 的屬性,都是 String 類型,換而言之,都是簡單類型,那假設,現在增加了一個屬性,履歷上需要有一個證明人,是 Witness 類型的對象,那大家一起來思考這樣一個問題,當再次進行原型拷貝的時候,屬性 witness 會怎麼去處理呢?是複制一份,還是讓一個引用去指向已有的 witness?覺知此事要躬行,下面,我們通過案例代碼來進行測試
案例示範
//履歷類
public class Resume implements Cloneable{
private String name;
private String position;
private String salary;
public Witness witness; //證明人
//構造器/get/set/toString...省略
}
//證明人類
public class Witness {
private String name;
private String job; //職務
//構造器/get/set/toString...省略
}
//使用原型模式測試
public class Client {
public static void main(String[] args) {
Resume resume = new Resume("小李", "海澱區", "面議");
resume.setWitness(new Witness("三叔","養豬場CTO"));
Resume clone = (Resume)resume.clone(); //使用原型模式克隆兩份
Resume clone1 = (Resume)resume.clone();
Resume clone2 = (Resume)resume.clone();
System.out.println(clone.getWitness().hashCode()); //測試
System.out.println(clone1.getWitness().hashCode());
System.out.println(clone2.getWitness().hashCode());
}
}
可以看到,我們使用原型模式克隆了三份,發現三個成員變量 witness 的哈希值是相同的,也就是說,預設并沒有對屬性 witness 進行拷貝,而是通過一個引用去指向已有的 witness,這種情況就稱為淺拷貝
淺拷貝
下面,我們來一起看一下淺拷貝的介紹
對于資料類型是基本資料類型的成員變量,淺拷貝會直接進行值傳遞,就是将該屬性值複制一份給新的對象
對于資料類型是引用資料類型的成員變量,比如說成員變量是某個數組、某個類的對象等,那麼淺拷貝會進行引用傳遞,也就是隻是将該成員變量的引用值(記憶體位址)複制一份給新的對象,實際上兩個對象的該成員變量都指向同一個執行個體,這種情況下,在一個對象中修改該成員變量會影響到另一個對象的該成員變量值
是以,我們前面講的小李求職的案例就是淺拷貝,淺拷貝就是使用預設的 clone() 方法實作的
那如果說,實際情況中,我們希望對于資料類型是引用資料類型的成員變量,也複制一份呢?那對比着再來聊一下深拷貝,以及深拷貝的實作方式
深拷貝
深拷貝的基本介紹如下
複制對象的所有基本資料類型的成員變量值
為所有引用資料類型的成員變量申請存儲空間,并複制每個引用資料類型成員變量所引用的對象,直到該對象可達的所有對象,也就是說,對象進行深拷貝要對整個對象(包括對象的引用類型)進行拷貝
深拷貝在實際開發中有兩種實作方式,一種是通過重寫 clone () 方法實作,一種是通過對象序列化實作
實作方式一:重寫 clone() 方法
示例代碼如下圖所示
public class DeepCloneableTarget implements Serializable, Cloneable {
private static final long serialVersionUID = 1L;
private String cloneName;
private String cloneClass;
public DeepCloneableTarget(String cloneName, String cloneClass) {
this.cloneName = cloneName;
this.cloneClass = cloneClass;
}
//因為該類的屬性都是String,是以我們使用預設的clone方法完成即可
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
public class DeepProtoType implements Serializable, Cloneable {
private String name; //String類型
private DeepCloneableTarget deepCloneableTarget; //引用類型
public DeepProtoType() {
super();
}
//深拷貝 - 方式一:重寫clone方法
@Override
protected Object clone() throws CloneNotSupportedException {
Object deep = null;
//這裡完成的是對基本資料類型以及String類型的拷貝
deep = super.clone();
//對引用類型的屬性進行單獨處理
DeepProtoType deepProtoType = (DeepProtoType) deep;
deepProtoType.deepCloneableTarget= (DeepCloneableTarget) deepCloneableTarget.clone();
return deepProtoType;
}
}
public class Client {
public static void main(String[] args) throws CloneNotSupportedException {
DeepProtoType deepProtoType = new DeepProtoType();
deepProtoType.name = "小李";
deepProtoType.deepCloneableTarget = new DeepCloneableTarget("老李","克隆類");
//方式一完成深拷貝
DeepProtoType clone = (DeepProtoType) deepProtoType.clone();
DeepProtoType clone1 = (DeepProtoType) deepProtoType.clone();
System.out.println(clone.deepCloneableTarget.hashCode());
System.out.println(clone1.deepCloneableTarget.hashCode()); //哈希值不一樣
}
}
可以看到,經測試,哈希值不一樣,說明确實整個對象(包括對象的引用類型)都進行了拷貝 ,實作了深拷貝
重寫 clone() 方法完成深拷貝的思路就是,分步進行,在 clone() 方法裡,首先調用 super.clone() 完成對基本類型和 String 類型的拷貝,因為對于基本資料類型的成員變量,深拷貝和淺拷貝的處理方式一緻,都是将該屬性值複制一份給新的對象,接着我們對引用類型的屬性進行單獨處理,同樣使用 clone() 方法完成屬性值的拷貝
那這樣有的小夥伴要問了,如果 DeepProtoType 有很多個引用類型的成員屬性,都要這樣挨個使用 clone() 方法完成屬性值的拷貝嘛?是的,這種情況下,每個引用類型需要單獨處理,那還有的小夥伴問了,如果引用類型是個類,裡面仍然有引用類型的成員屬性呢?
級聯處理示範
如果引用類型是個class,類型,裡面仍然有引用類型的成員屬性同樣,核心思想是分布單獨處理,假設類 DeepProtoType 裡有成員屬性 deepCloneableTarget ,而DeepCloneableTarget 類中有成員屬性 witness ,同樣使用方式一,重寫 clone() 方法完成深拷貝,示例代碼如下圖所示
//方式一更新:測試級聯克隆
public class DeepProtoType implements Serializable, Cloneable {
public String name; //String類型
public DeepCloneableTarget deepCloneableTarget; //引用類型
public DeepProtoType() {
super();
}
//深拷貝 - 方式一:重寫clone方法 級聯
@Override
protected Object clone() throws CloneNotSupportedException {
Object deep = null;
//這裡完成的是對基本資料類型以及String類型的拷貝
deep = super.clone();
//對引用類型的屬性進行單獨處理
DeepProtoType deepProtoType = (DeepProtoType) deep;
deepProtoType.deepCloneableTarget= (DeepCloneableTarget) deepCloneableTarget.clone();
deepProtoType.deepCloneableTarget.witness = (Witness) deepProtoType.deepCloneableTarget.witness.clone();
return deepProtoType;
}
@Override
public String toString() {
return "DeepProtoType{" +
"name='" + name + '\'' +
", deepCloneableTarget=" + deepCloneableTarget +
'}';
}
}
public class DeepCloneableTarget implements Serializable, Cloneable {
private static final long serialVersionUID = 1L;
private String cloneName;
private String cloneClass;
public Witness witness; //增加引用類型
public DeepCloneableTarget(String cloneName, String cloneClass) {
this.cloneName = cloneName;
this.cloneClass = cloneClass;
}
//因為該類的屬性都是String,是以我們使用預設的clone方法完成即可
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
@Override
public String toString() {
return "DeepCloneableTarget{" +
"cloneName='" + cloneName + '\'' +
", cloneClass='" + cloneClass + '\'' +
", witness=" + witness +
'}';
}
}
//證明人類
public class Witness implements Serializable,Cloneable {
private String name;
private String job; //職務
public Witness(String name, String job) {
this.name = name;
this.job = job;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getJob() {
return job;
}
public void setJob(String job) {
this.job = job;
}
@Override
public String toString() {
return "Witness{" +
"name='" + name + '\'' +
", job='" + job + '\'' +
'}';
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
//方式一更新:測試級聯克隆
public class Client {
public static void main(String[] args) throws CloneNotSupportedException {
DeepProtoType deepProtoType = new DeepProtoType();
deepProtoType.name = "小李";
deepProtoType.deepCloneableTarget = new DeepCloneableTarget("老李","克隆類");
deepProtoType.deepCloneableTarget.witness = new Witness("證明人", "職務");
DeepProtoType clone = (DeepProtoType) deepProtoType.clone();
DeepProtoType clone1 = (DeepProtoType) deepProtoType.clone();
System.out.println(clone);
System.out.println(clone1);
System.out.println(clone.deepCloneableTarget.witness.hashCode());
System.out.println(clone1.deepCloneableTarget.witness.hashCode());
//測試哈希值不一樣 實作了深拷貝
}
}
實作方式二:通過對象序列化
上面我們示範了方式一,實作了深拷貝,但同時也帶來了問題,如果有很多個引用類型的成員屬性,或者說(引用類型是個類,裡面仍然有引用類型的成員屬性)這種級聯的情況,需要分步處理,如果要拷貝的對象結構複雜,實作起來非常繁瑣,那有沒有可以一步到位的方法呢?下面我們來示範第二種,使用對象序列化的形式,示例代碼如
下圖所示
public class DeepProtoType implements Serializable, Cloneable {
public String name; //String類型
public DeepCloneableTarget deepCloneableTarget; //引用類型
public DeepProtoType() {
super();
}
//深拷貝 - 方式二:通過對象序列化(推薦的)
public Object deepClone(){
//建立流對象
ByteArrayOutputStream bos = null;
ObjectOutputStream oos = null;
ByteArrayInputStream bis = null;
ObjectInputStream ois = null;
try {
//序列化
bos = new ByteArrayOutputStream();
oos = new ObjectOutputStream(bos);
oos.writeObject(this);//将目前對象,以對象流的方式輸出(即序列化)
//輸出的時候會把包括引用類型一起輸出
//反序列化
bis = new ByteArrayInputStream(bos.toByteArray());//把輸出去的對象再讀進來
ois = new ObjectInputStream(bis);
DeepProtoType clone = (DeepProtoType)ois.readObject();
//充分利用序列化和反序列化的特點,把目前對象this以對象流的方式輸出去,然後以對象的方式再讀回來,關聯的引用類型自然也讀回來了
return clone;
} catch (Exception e) {
e.getMessage();
} finally {
//關閉流操作
try {
bos.close();
oos.close();
bis.close();
ois.close();
} catch (IOException e) {
e.getMessage();
}
}
return null;
}
@Override
public String toString() {
return "DeepProtoType{" +
"name='" + name + '\'' +
", deepCloneableTarget=" + deepCloneableTarget +
'}';
}
}
public class DeepCloneableTarget implements Serializable, Cloneable {
/**
*
*/
private static final long serialVersionUID = 1L;
private String cloneName;
private String cloneClass;
public DeepCloneableTarget(String cloneName, String cloneClass) {
this.cloneName = cloneName;
this.cloneClass = cloneClass;
}
//因為該類的屬性都是String,是以我們使用預設的clone方法完成即可
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
@Override
public String toString() {
return "DeepCloneableTarget{" +
"cloneName='" + cloneName + '\'' +
", cloneClass='" + cloneClass + '\'' +
'}';
}
}
// 測試 方式二 通過對象序列化
public class Client {
public static void main(String[] args) throws CloneNotSupportedException {
DeepProtoType deepProtoType = new DeepProtoType();
deepProtoType.name = "小李";
deepProtoType.deepCloneableTarget = new DeepCloneableTarget("老李","克隆類");
DeepProtoType clone = (DeepProtoType)deepProtoType.deepClone();
DeepProtoType clone1 = (DeepProtoType)deepProtoType.deepClone();
System.out.println(clone.toString());
System.out.println(clone1.toString());
System.out.println(clone.deepCloneableTarget.hashCode());
System.out.println(clone1.deepCloneableTarget.hashCode()); //測試哈希值不一緻
}
}
可以看到,經測試,哈希值不一樣,說明确實整個對象(包括對象的引用類型)都進行了拷貝 ,我們通過對象序列化的方式也實作了深拷貝,而且利用序列化和反序列化的特點,把目前對象this以對象流的方式輸出去,然後以對象的方式再讀回來,關聯的引用類型的屬性值自然也讀回來了,這種方式是推薦大家使用的,因為是直接把目前整個對象進行序列化和反序列化操作,那不管類的結構如何複雜,都可以通過對象序列化的方式整體處理
注意事項
原型模式到這裡就告一段落了,下面我們一起來總結下原型模式的注意事項,分析下優劣勢以及應用場景
優勢
建立新的對象比較複雜時,可以利用原型模式簡化對象的建立過程,同時也可以提高效率
不用重新初始化對象,而是動态地獲得對象運作時的狀态
如果原始對象發生變化(增加或者減少屬性),其它克隆對象的也會發生相應的變化,無需修改代碼
劣勢
需要為每一個類配備一個克隆方法,這對全新的類來說不是很難,但對已有的類進行改造時,需要修改其源代碼,會違背 ocp 原則,這點需要和小夥伴們提一下
下節預告
OK, 下一節我們進入下一個模式 - 建造者模式的學習,希望大家在學習的過程中,能夠感覺到設計模式的有趣之處,高效而愉快的學習,那我們下期見~