跳转至

原型模式(Prototype Pattern)

一句话记忆口诀:通过克隆已有对象创建新对象,避免重复初始化的开销,注意浅拷贝 vs 深拷贝的陷阱。


1. 引入:它解决了什么问题?

没有原型模式时的问题

当创建对象的成本很高(如需要复杂计算、数据库查询、网络请求),或者需要创建大量相似对象时:

// ❌ 反例:每次都从数据库加载配置创建对象
public class ReportGenerator {
    public Report createReport(String type) {
        // 每次都要查数据库获取模板配置 —— 耗时操作!
        ReportTemplate template = db.queryTemplate(type);
        // 每次都要解析模板 —— CPU 密集操作!
        Report report = parseTemplate(template);
        // 每次都要加载默认样式 —— IO 操作!
        report.loadDefaultStyles();
        return report;
    }
}

// 如果需要生成 100 份同类型报表,上述代码会执行 100 次数据库查询!

问题根因: 1. 重复执行昂贵的初始化操作(数据库查询、文件解析、网络请求) 2. 无法高效创建大量相似对象(只有少量属性不同) 3. 创建逻辑与业务逻辑耦合,难以扩展新类型

工作中的典型应用场景

场景 Spring/JDK 中的例子
Bean 定义复制 Spring BeanDefinition 的 clone
对象缓存预热 缓存原型对象,需要时克隆后修改
线程安全的对象创建 克隆线程安全的原型,避免共享可变状态
测试数据构建 克隆基础测试对象,修改个别字段
配置对象复制 基于默认配置克隆后定制化修改

2. 类比:用生活模型建立直觉

生活类比:复印文件

你有一份精心排版的合同模板(原型对象),每次签新客户时: - 不用原型模式:从头打字排版一份新合同(重新创建对象)—— 耗时且容易出错 - 用原型模式:复印一份模板,只修改客户姓名和金额(克隆后修改)—— 快速且一致

关键点: - 浅拷贝 = 黑白复印:文字复制了,但附件只是引用(指向同一份附件原件) - 深拷贝 = 彩色复印 + 附件也复印:所有内容都是独立副本

抽象定义

原型模式用一个已经创建的实例作为原型,通过复制该原型对象来创建一个和原型相同或相似的新对象,而无需知道其具体创建细节。


3. 原理:逐步拆解核心机制

UML 类图

classDiagram
    class Prototype {
        <<interface>>
        +clone() Prototype
    }
    class ConcretePrototypeA {
        -field1 String
        -field2 List
        +clone() Prototype
    }
    class ConcretePrototypeB {
        -fieldX int
        +clone() Prototype
    }
    class Client {
        -prototype Prototype
        +createObject() Prototype
    }

    Prototype <|.. ConcretePrototypeA
    Prototype <|.. ConcretePrototypeB
    Client --> Prototype

实现方式一:Java Cloneable 接口(浅拷贝)

// ===== 原型类 =====
public class ReportTemplate implements Cloneable {
    private String title;
    private String type;
    private List<String> sections;  // 引用类型字段

    public ReportTemplate(String title, String type, List<String> sections) {
        this.title = title;
        this.type = type;
        this.sections = sections;
        // 模拟耗时的初始化操作
        System.out.println("执行耗时初始化:加载模板配置...");
    }

    // 浅拷贝:基本类型复制值,引用类型复制引用(共享同一对象)
    @Override
    public ReportTemplate clone() {
        try {
            return (ReportTemplate) super.clone();
        } catch (CloneNotSupportedException e) {
            throw new RuntimeException("克隆失败", e);
        }
    }

    // getter/setter 省略
}

// ===== 使用示例 =====
public class Main {
    public static void main(String[] args) {
        // 只执行一次耗时初始化
        ReportTemplate prototype = new ReportTemplate(
            "月度报表", "MONTHLY", new ArrayList<>(Arrays.asList("概述", "数据", "结论"))
        );

        // 克隆 —— 不会再执行耗时初始化!
        ReportTemplate report1 = prototype.clone();
        report1.setTitle("1月报表");

        ReportTemplate report2 = prototype.clone();
        report2.setTitle("2月报表");

        // ⚠️ 浅拷贝陷阱:修改 sections 会影响所有克隆对象!
        report1.getSections().add("附录");
        // prototype、report2 的 sections 也被修改了!
    }
}

实现方式二:深拷贝(序列化方式)

public class ReportTemplate implements Serializable {
    private String title;
    private String type;
    private List<String> sections;

    // 通过序列化实现深拷贝 —— 所有引用类型字段都会被完整复制
    public ReportTemplate 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 (ReportTemplate) ois.readObject();
        } catch (Exception e) {
            throw new RuntimeException("深拷贝失败", e);
        }
    }
}

实现方式三:原型注册表(Prototype Registry)

// ===== 原型注册表:缓存预创建的原型对象 =====
public class PrototypeRegistry {
    private static final Map<String, ReportTemplate> registry = new HashMap<>();

    // 应用启动时预热:创建各类型的原型对象
    static {
        registry.put("MONTHLY", new ReportTemplate("月度报表", "MONTHLY",
            new ArrayList<>(Arrays.asList("概述", "数据", "结论"))));
        registry.put("WEEKLY", new ReportTemplate("周报", "WEEKLY",
            new ArrayList<>(Arrays.asList("本周进展", "下周计划"))));
    }

    // 获取时返回克隆对象,保护原型不被修改
    public static ReportTemplate getTemplate(String type) {
        ReportTemplate prototype = registry.get(type);
        if (prototype == null) {
            throw new IllegalArgumentException("未知模板类型: " + type);
        }
        return prototype.deepClone();
    }
}

核心流程图

flowchart TD
    A[Client 需要新对象] --> B{原型注册表中<br/>是否有对应原型?}
    B -->|有| C[克隆原型对象]
    B -->|无| D[创建新对象并注册为原型]
    C --> E{需要深拷贝?}
    E -->|是| F[序列化/反序列化<br/>完整复制所有层级]
    E -->|否| G[Object.clone<br/>浅拷贝]
    F --> H[修改克隆对象的<br/>差异化属性]
    G --> H
    D --> H
    H --> I[返回定制化的新对象]

    style I fill:#9f9,stroke:#333

4. 特性:关键对比

浅拷贝 vs 深拷贝

对比维度 浅拷贝(Shallow Copy) 深拷贝(Deep Copy)
基本类型 ✅ 复制值 ✅ 复制值
引用类型 ❌ 复制引用(共享对象) ✅ 递归复制(独立对象)
性能 ✅ 快(只复制一层) ❌ 慢(递归复制所有层)
安全性 ❌ 修改会互相影响 ✅ 完全独立
实现方式 Object.clone() 序列化 / 手动递归复制

原型模式 vs 工厂模式

对比维度 原型模式 工厂模式
创建方式 克隆已有对象 通过 new 创建新对象
适用场景 创建成本高、对象相似度高 根据条件创建不同类型对象
初始化 跳过初始化(已在原型中完成) 每次都执行初始化
灵活性 运行时动态添加/删除原型 编译时确定产品类型

在 Spring / JDK 中的应用

框架/类 说明
Object.clone() JDK 原生克隆支持(浅拷贝)
ArrayList.clone() 列表浅拷贝
Spring scope="prototype" 每次获取 Bean 时创建新实例(概念相关,非严格原型模式)
Spring BeanDefinition Bean 定义的复制与合并
Arrays.copyOf() 数组复制

5. 边界:异常情况与常见误区

误区一:浅拷贝导致数据污染(运行期问题)

// ❌ 错误:浅拷贝后修改引用类型字段,影响了原型对象
ReportTemplate prototype = new ReportTemplate("模板", "DEFAULT",
    new ArrayList<>(Arrays.asList("章节1", "章节2")));

ReportTemplate copy = prototype.clone(); // 浅拷贝
copy.getSections().add("章节3");

// prototype 的 sections 也变成了 ["章节1", "章节2", "章节3"]!
// 原因:浅拷贝只复制了 List 的引用,两个对象共享同一个 List

// ✅ 正确:对包含引用类型字段的对象使用深拷贝
ReportTemplate copy = prototype.deepClone();
copy.getSections().add("章节3"); // 不影响 prototype

误区二:clone() 方法不调用构造方法(设计问题)

// ⚠️ 注意:clone() 不会调用任何构造方法!
public class Singleton implements Cloneable {
    private static final Singleton INSTANCE = new Singleton();
    private Singleton() { System.out.println("构造方法被调用"); }

    @Override
    public Singleton clone() {
        return (Singleton) super.clone(); // 不会调用构造方法!
    }
}

// 这意味着:
// 1. 单例模式的类不应该实现 Cloneable,否则可以通过 clone 破坏单例
// 2. 如果构造方法中有重要的初始化逻辑,clone 后的对象可能状态不完整

// ✅ 防御措施:在 clone() 中抛出异常
@Override
public Object clone() throws CloneNotSupportedException {
    throw new CloneNotSupportedException("单例对象不允许克隆");
}

误区三:循环引用导致深拷贝栈溢出(运行期问题)

// ❌ 错误:对象之间存在循环引用时,手动递归深拷贝会栈溢出
public class Node {
    private String name;
    private Node next;  // 如果 A.next = B, B.next = A,递归复制会死循环

    // ✅ 解决方案:使用序列化方式深拷贝(Java 序列化能正确处理循环引用)
    // 或者使用 Map 记录已复制的对象,避免重复复制
}

6. 总结:面试标准化表达

高频问题

Q1:原型模式解决了什么问题?

原型模式解决了对象创建成本高的问题。当创建对象需要复杂的初始化操作(如数据库查询、文件解析、网络请求)时,通过克隆已有的原型对象来创建新对象,可以跳过耗时的初始化过程。同时,原型模式也适用于需要创建大量相似对象的场景,只需克隆后修改差异化属性即可。

Q2:浅拷贝和深拷贝的区别?

浅拷贝只复制对象的第一层:基本类型复制值,引用类型复制引用(新旧对象共享同一个引用对象)。深拷贝递归复制所有层级,新旧对象完全独立。Java 中 Object.clone() 默认是浅拷贝,深拷贝可以通过序列化/反序列化或手动递归复制实现。工作中需要根据对象是否包含可变引用类型字段来选择拷贝方式。

Q3:原型模式和 Spring 的 prototype 作用域有什么关系?

Spring 的 scope="prototype" 表示每次获取 Bean 时都创建新实例,这与原型模式的"克隆"思想有概念上的相似性(都是创建新对象),但实现方式不同:Spring prototype 作用域是通过反射调用构造方法创建新对象,而原型模式是通过克隆已有对象。严格来说,Spring 的 prototype 作用域不是原型模式的实现。


一句话记忆口诀:通过克隆已有对象创建新对象,避免重复初始化的开销,浅拷贝共享引用、深拷贝完全独立,Object.clone() 不调用构造方法。