當創建對象成本高昂 (如大對象、需查詢數據庫、複雜計算) 或系統需動態增減產品類時,原型模式 通過複製現有原型對象 (clone()) 來創建新實例,避免繁瑣的構造過程。本文剖析簡單/登記兩種形式、Cloneable 機制、淺/深拷貝本質區別、Externalizable 繞過 transient 的特例,並給出最佳實踐。
| 角色 | 職責 |
|---|---|
| Client (客戶) | 請求複製原型 |
| Prototype (抽象原型) | 聲明 clone() 接口 |
| ConcretePrototype (具體原型) | 實現 clone(),支持複製 |
| PrototypeManager (原型管理器) | 登記形式專用:緩存原型、按鍵獲取 |
// 1. 抽象原型
interface Prototype extends Cloneable {
Prototype clone() throws CloneNotSupportedException;
}
// 2. 具體原型
class ConcretePrototype implements Prototype {
private String field;
public ConcretePrototype(String field) { this.field = field; }
@Override public Prototype clone() {
try { return (Prototype) super.clone(); } // Object.clone() 是淺拷貝
catch (CloneNotSupportedException e) { throw new AssertionError(); }
}
}
// 3. 客戶端
class Client {
public void operation(Prototype prototype) {
Prototype copy = prototype.clone(); // 直接複製
// 使用 copy...
}
}
適用:原型種類少、固定、無需集中管理。
class PrototypeManager {
private Map registry = new HashMap<>();
public void register(String key, Prototype prototype) {
registry.put(key, prototype);
}
public Prototype get(String key) {
return registry.get(key).clone(); // 返回複製品,保護原型
}
}
// 客戶端
Client client = new Client();
PrototypeManager pm = new PrototypeManager();
pm.register("typeA", new ConcretePrototype("A"));
Prototype p = pm.get("typeA"); // 獲取克隆實例
適用:原型動態增減、運行時決定類型、需集中控制實例創建。
class Address { String city; }
class User implements Cloneable {
String name; Address addr;
@Override protected User clone() {
try { return (User) super.clone(); } catch (Exception e) { throw new Error(e); }
}
}
User u1 = new User(); u1.name = "Tom"; u1.addr = new Address(); u1.addr.city = "Beijing";
User u2 = u1.clone();
u2.name = "Jerry"; // 基本類型獨立
u2.addr.city = "Shanghai"; // 引用類型共享!
System.out.println(u1.addr.city); // Shanghai ← 原對象被修改!
| 方式 | 原理 | 優點 | 缺點 |
|---|---|---|---|
| 重寫 clone 遞歸複製 | 在 clone() 中手動 new 所有引用字段 | 可控、無依賴 | 代碼繁瑣、易漏字段、循環引用需特殊處理 |
| 序列化/反序列化 | 實現 Serializable,寫入字節流再讀出 | 自動處理整個對象圖、支持循環引用 | 性能較差、需所有類 Serializable、transient 字段會丟失 |
| 第三方庫 (推薦) | Apache Commons Lang SerializationUtils.clone() 或 JSON 轉換 (Gson/Jackson) |
一行代碼、JSON 可跨語言、性能尚可 | 引入依賴、JSON 需無參構造器 |
// 方式 1:手動深拷貝
@Override protected User clone() {
User copy = (User) super.clone();
copy.addr = new Address(this.addr.city); // 手動複製 Address
return copy;
}
// 方式 2:序列化深拷貝 (工具類)
public static T deepClone(T obj) {
try (ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos)) {
oos.writeObject(obj);
try (ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis)) {
return (T) ois.readObject();
}
} catch (Exception e) { throw new RuntimeException(e); }
}
// 方式 3:JSON 深拷貝 (Gson)
User u2 = new Gson().fromJson(new Gson().toJson(u1), User.class);
實現 Externalizable 時,序列化完全由 writeExternal/readExternal 手動控制,transient 失效:
class ExternalizableDemo implements Externalizable {
private transient String content = "我被 transient 修飾";
@Override public void writeExternal(ObjectOutput out) throws IOException {
out.writeObject(content); // 手動寫入,照樣序列化!
}
@Override public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
content = (String) in.readObject();
}
}
// 輸出: "我被 transient 修飾"
結論:Externalizable 給予完全控制權,transient 僅對默認 Serializable 機制生效。
| 優點 | 缺點 |
|---|---|
| 動態增減產品類,無需修改代碼 | 每個類都需實現 clone(),侵入性強 |
| 簡化創建結構,無需工廠層級結構 | 深拷貝複雜,循環引用、transient 字段易出錯 |
| 支持運行時動態加載新功能 | 構造函數不會被調用,可能跳過初始化邏輯 |
| 無需預先確定類層次 | 克隆對象可能攜帶不該共享的資源 (連接、鎖、線程) |
CloneNotSupportedException 改為運行時異常。原型模式 = Cloneable + clone() + (可選) 管理器。核心難點在於淺/深拷貝選擇:基本類型/不可變對象淺拷貝無傷大雅,可變引用對象必須深拷貝。現代 Java 開發中,JSON 序列化深拷貝 是性能與開發效率的最佳平衡點。記住:克隆是手段,解決創建成本高/動態類型才是目的。