1.前言
單例模式可以避免重復創建消耗資源的對象,但是卻不得不共用對象。若是對象本身也不讓隨意訪問修改時,怎么辦?通常做法是備份到副本,其它對象操作副本,最后獲取權限合并,類似git上的PR操作。
2.概念
原型模式用原型實例指定創建對象的種類,并通過拷貝這些原型創建新的對象。需要注意的關鍵字是,新的對象,類沒變。Java正好提供了Cloneable接口,它標識的類可以調用Object中實現的clone()方法而不拋出異常,即運行時通知虛擬機可以安全使用clone()方法返回拷貝對象。由于它直接操作內存中的二進制流,當大量操作或操作復雜對象時,性能優勢將會很明顯。
3.場景
動物園中有一只羊,對它進行克隆,產生另外一只完全一樣的羊,分別安排兩位有孩子的管理員照顧。有一天,對克隆羊進行基因操作,觀察變化。
4.寫法
// 1.聲明此類可以被clone
public class Sheep implements Cloneable {
private int age;
private String sex;
private Admin admin;
public Sheep(int age, String sex, Admin admin) {
super();
this.age = age;
this.sex = sex;
this.admin = admin;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
public Admin getAdmin() {
return admin;
}
public void setAdmin(Admin admin) {
this.admin = admin;
}
@Override
public String toString() {
return "Sheep [age=" + age + ", sex=" + sex + ", admin=" + admin + "]";
}
// 2.調用Object的clone方法
public Sheep startClone() {
Sheep sheep = null;
try {
sheep = (Sheep) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return sheep;
}
}
public class Admin {
private int age;
private String sex;
private Child child;
public Admin(int age, String sex, Child child) {
super();
this.age = age;
this.sex = sex;
this.child = child;
}
public void setAge(int age) {
this.age = age;
}
public void setSex(String sex) {
this.sex = sex;
}
public void setChild(Child child) {
this.child = child;
}
@Override
public String toString() {
return "Admin [age=" + age + ", sex=" + sex + ", child=" + child + "]";
}
}
public class Child {
}
public class Zoo {
public static void main(String[] args) {
Sheep old = new Sheep(2, "雄性", new Admin(25, "女", new Child()));
System.out.println(old.toString());
Sheep current = old.startClone();
System.out.println(current.toString());
// 對克隆羊做處理
current.setAge(1);
current.setSex("雌性");
current.getAdmin().setAge(34);
current.getAdmin().setSex("男");
System.out.println(old.toString());
System.out.println(current.toString());
}
}
根據上面的代碼,我們知道羊引用了管理員,管理員引用了孩子。當對內存中數據拷貝時,除了基本數據類型(包括封裝類型)及String類型,其它的引用關系將直接傳遞給副本,并不是重新創建一個對象。所以當對克隆羊操作時,年齡和性別直接改變,而對管理員的操作將尋址到內存中對應部分進行修改,導致原型也被修改。孩子與管理員的關系就如同管理員與羊,通過哈希值可以知道,孩子始終就一個,沒有拷貝成功。
上面的錯誤是由于只拷貝了最外層對象的原因,我們稱之為淺拷貝。為了解決這個問題,需要對內部的引用類型進行拷貝(Java中大部分引用類型實現了Cloneable接口,可以方便的拷貝),具體如下:
// 1.聲明此類可以被clone
public class Sheep implements Cloneable {
// 前面省略
// 2.調用Object的clone方法
public Sheep startClone() {
Sheep sheep = null;
try {
sheep = (Sheep) super.clone();
// 3.調用Admin的clone方法
sheep.admin = this.admin.startClone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return sheep;
}
}
public class Admin implements Cloneable {
// 前面省略
public Admin startClone() {
Admin admin = null;
try {
admin = (Admin) super.clone();
admin.child = this.child.startClone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return admin;
}
}
public class Child implements Cloneable {
public Child startClone() {
Child child = null;
try {
child = (Child) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return child;
}
}
通過日志的打印,發現這種方式(深拷貝)起作用了。由1、2行可以知道,拷貝時引用類型已經重新創建了對象。由3、4行可以知道,修改其中一個對象不會再改變另一個了。
5.總結
原型模式通過Object的clone()方法實現,由于是內存操作,無視構造方法和訪問權限,直接獲取新的對象。但對于引用類型,需使用深拷貝,其它淺拷貝即可。