原型模式
原型模式(Prototype Pattern) 是指原型實例指定創建對象的種類, 並且通過拷貝這些原型創建新
的對象,屬於創建型模式。
官方原文:Specify the kinds of objects to create using a prototypical instance, and create new objects by copying this prototype.
原型模式的核心在於拷貝原型對象。以系統中已存在的一個對象爲原型,直接基於內存二進制流進
行拷貝,無需再經歷耗時的對象初始化過程(不調用構造函數),性能提升許多。當對象的構建過程比
較耗時時,可以利用當前系統中已存在的對象作爲原型,對其進行克隆(一般是基於二進制流的複製)
通用的類圖如下:
注:對不通過new關鍵字, 而是通過對象拷貝來實現創建對象的模式就稱作原型模式。
原型模式的應用場景:
1、類初始化消耗資源較多。
2、new產生的一個對象需要非常繁瑣的過程(數據準備、訪問權限等)
3、構造函數比較複雜。
4、循環體中生產大量對象時。
在Spring中, 原型模式應用得非常廣泛。例如scope=”prototype”, 在我們經常用的JSON.parse Object(也是一種原型模式。
原型模式的通用寫法
一個標準的原型模式代碼, 應該是這樣設計的。先創建原型I Prototype接口;創建具體需要克隆的對象Concrete Prototype;
如下:
原型接口:
public interface IPrototype<T> {
T clone();
}
需要克隆的對象:
@Data
public class ConcretePrototype implements IPrototype {
private int age;
private String name;
@Override
public ConcretePrototype clone() {
ConcretePrototype concretePrototype = new ConcretePrototype();
concretePrototype.setAge(this.age);
concretePrototype.setName(this.name);
return concretePrototype;
}
@Override
public String toString() {
return "ConcretePrototype{" +
"age=" + age +
", name='" + name + '\'' +
'}';
}
}
調用方:
public class Client {
public static void main(String[] args) {
//創建原型對象
ConcretePrototype prototype = new ConcretePrototype();
prototype.setAge(18);
prototype.setName("Tom");
System.out.println(prototype);
//拷貝原型對象
ConcretePrototype cloneType = prototype.clone();
System.out.println(cloneType);
}
}
在這個簡單的場景之下,看上去操作好像變複雜了。但如果有幾百個屬性需要複製,那我們就可以一勞永逸。但是,上面的複製過程是我們自己完成的, 在實際編碼中, 我們一般不會浪費這樣的體力勞動, JDK已經幫我們實現了一個現成的API, 我們只需要實現Cloneable接口即可:
如下覆蓋中的方法使用父類clone方法:
@Override
public ConcretePrototype clone() {
try {
return (ConcretePrototype)super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
return null;
}
}
下面我們再來做一個測試, 給Concrete Prototype增加一個個人愛好的屬性hobbies:
import lombok.Data;
import java.util.List;
@Data
public class ConcretePrototype implements Cloneable {
private int age;
private String name;
private List<String> hobbies;
@Override
public ConcretePrototype clone() {
try {
return (ConcretePrototype)super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
return null;
}
}
@Override
public String toString() {
return "ConcretePrototype{" +
"age=" + age +
", name='" + name + '\'' +
", hobbies=" + hobbies +
'}';
}
}
再次測試入下:
import java.util.ArrayList;
import java.util.List;
public class Client {
public static void main(String[] args) {
//創建原型對象
ConcretePrototype prototype = new ConcretePrototype();
prototype.setAge(18);
prototype.setName("dxy");
List<String> hobbies = new ArrayList<String>();
hobbies.add("games");
hobbies.add("reading");
prototype.setHobbies(hobbies);
//拷貝原型對象
ConcretePrototype cloneType = prototype.clone();
cloneType.getHobbies().add("riding");
System.out.println("原型對象:" + prototype);
System.out.println("克隆對象:" + cloneType);
System.out.println(prototype == cloneType);
System.out.println("原型對象的愛好:" + prototype.getHobbies());
System.out.println("克隆對象的愛好:" + cloneType.getHobbies());
System.out.println(prototype.getHobbies() == cloneType.getHobbies());
}
}
測試結果:
我們給,複製後的克隆對象新增一項愛好,發現原型對象也發生了變化,這顯然不符合我們的預期。因爲我們希望克隆出來的對象應該和原型對象是兩個獨立的對象,不應該再有聯繫了。從測試結果分析來看, 應該是hobbies共用了一個內存地址, 意味着複製的不是值, 而是引用的地址。這樣的話, 如果我們修改任意一個對象中的屬性值, prototype和clone Type的hobbies值都會改變。這就是我們常說的淺克隆。只是完整複製了值類型數據,沒有賦值引用對象。換言之,所有的引用對象仍然指向原來的對象,顯然不是我們想要的結果。那如何解決這個問題呢?下面我們來看深度克隆繼續改造。
使用序列化實現深度克隆:
@Data
public class ConcretePrototype implements Cloneable,Serializable {
private int age;
private String name;
private List<String> hobbies;
@Override
public ConcretePrototype clone() {
try {
return (ConcretePrototype)super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
return null;
}
}
public ConcretePrototype deepCloneHobbies(){
try {
ConcretePrototype result = (ConcretePrototype)super.clone();
result.hobbies = (List)((ArrayList)result.hobbies).clone();
return result;
} catch (CloneNotSupportedException e) {
e.printStackTrace();
return null;
}
}
public ConcretePrototype deepClone(){
try {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(this);
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis);
return (ConcretePrototype)ois.readObject();
}catch (Exception e){
e.printStackTrace();
return null;
}
}
@Override
public String toString() {
return "ConcretePrototype{" +
"age=" + age +
", name='" + name + '\'' +
", hobbies=" + hobbies +
'}';
}
}
測試:
public class Client {
public static void main(String[] args) {
//創建原型對象
ConcretePrototype prototype = new ConcretePrototype();
prototype.setAge(18);
prototype.setName("dxy");
List<String> hobbies = new ArrayList<String>();
hobbies.add("music");
hobbies.add("book");
prototype.setHobbies(hobbies);
//拷貝原型對象
ConcretePrototype cloneType = prototype.deepCloneHobbies();
cloneType.getHobbies().add("swing");
System.out.println("原型對象:" + prototype);
System.out.println("克隆對象:" + cloneType);
System.out.println(prototype == cloneType);
System.out.println("原型對象的愛好:" + prototype.getHobbies());
System.out.println("克隆對象的愛好:" + cloneType.getHobbies());
System.out.println(prototype.getHobbies() == cloneType.getHobbies());
}
}
結果:
克隆破壞單例模式
如果我們克隆的目標的對象是單例對象,那意味着,深克隆就會破壞單例。實際上防止克隆破壞單例解決思路非常簡單, 禁止深克隆便可。要麼你我們的單例類不實現Cloneable接口; 要麼我們重寫clone) 方法, 在clone方法中返回單例對象即可, 具體代碼如下:
import lombok.Data;
import java.util.List;
@Data
public class ConcretePrototype implements Cloneable {
private int age;
private String name;
private List<String> hobbies;
private static ConcretePrototype instance = new ConcretePrototype();
private ConcretePrototype(){}
public static ConcretePrototype getInstance(){
return instance;
}
@Override
public ConcretePrototype clone() {
return instance;
}
@Override
public String toString() {
return "ConcretePrototype{" +
"age=" + age +
", name='" + name + '\'' +
", hobbies=" + hobbies +
'}';
}
}