首页
/ CS-Notes 设计模式解读:原型模式(Prototype)的原理、实现与 JDK 原型应用

CS-Notes 设计模式解读:原型模式(Prototype)的原理、实现与 JDK 原型应用

2026-09-06 18:51:46作者:董灵辛Dennis

原型模式(Prototype Pattern)是创建型设计模式之一,它通过"复制一个已有实例(原型)"来产生新对象,而不是通过 new 重新构造。本文以 CS-Notes 仓库中 设计模式 - 原型模式 一讲为核心骨架,结合 Java 语言特性、Object.clone()Cloneable 契约,以及浅拷贝与深拷贝的边界问题,讲解原型模式如何做到"以复制代替构造",并给出可直接运行、可复制的完整代码示例与面试级辨析结论。读完本文,你将能够独立完成原型模式的 Java 实现,理解 JDK 内建克隆机制的限制,并在合适的业务场景中正确选用原型模式。

一、模式意图:用复制代替构造

使用原型实例指定要创建对象的类型,通过复制这个原型来创建新对象。

这是 设计模式 - 原型模式 中对该模式意图的原始定义,也是理解整个模式的一把钥匙。它包含两个关键动作:

  1. 先用一次完整的构造过程创建出一个"样板"对象,即原型实例;
  2. 此后所有新对象不再走构造函数,而是调用原型对象的克隆方法复制生成

在 Java 这类语言中,new 触发构造意味着必须经历完整的构造逻辑,而原型对象可能是一个已经被填充了大量默认状态、配置完成、甚至经过昂贵初始化的对象。原型模式把"构造"与"复制"解耦:构造只发生一次,复制可以发生无数次,从而在对象创建成本高、或创建参数复杂时降低开销,同时把对象的创建过程封装在对象自身内部。

需要特别强调的是,原型模式解决的核心矛盾是 "客户端面对抽象类型,却需要按已有实例的状态批量生成新实例"。客户端只依赖抽象原型类型,并不知道也不需要知道具体子类的构造细节。

二、类图:四个角色的职责划分

原型模式的标准类图如下(原文档图示):

原型模式 UML 类图:Client 依赖抽象 Prototype,ConcretePrototype1/2 继承并实现 clone 方法

从类图可以看出原型模式由四类角色构成,分别对应原文档的 Class Diagram 部分:

角色 名称 职责
抽象原型 Prototype 声明克隆方法的抽象接口,是所有具体原型的统一父类型,让客户端能够面向抽象编程
具体原型 ConcretePrototype1 / ConcretePrototype2 实现克隆方法,返回一份携带自身状态的新副本
客户端 Client 持有一个原型引用,通过调用其克隆方法获得新对象,不直接参与对象内部构造
复制产物 新对象 由克隆方法在运行时产生的、状态与原型一致但与原型完全独立的新实例

类图的核心信息可以概括为一句话:Client 只与抽象的 Prototype 打交道,克隆行为的具体实现被下放到各个 ConcretePrototype 子类中。这意味着当系统新增一种原型子类时,客户端代码无需任何改动即可继续工作,符合开闭原则(Open-Closed Principle)。

三、基本实现:原文档代码逐段精讲

CS-Notes 原型模式一节给出了一个非常精炼的 Java 实现骨架,下面逐一展开。

3.1 抽象原型:定义克隆契约

public abstract class Prototype {
    abstract Prototype myClone();
}

要点拆解:

  • Prototype 被声明为抽象类,它本身不实现克隆逻辑,只把 myClone() 作为"契约"约束所有子类必须提供克隆能力;
  • 方法名为 myClone(),用于避开与 JDK 内建的 Object.clone() 直接冲突(后者受 protectedCloneable 标记双重限制,详见第四节);
  • 返回类型是抽象类型 Prototype 而非具体类型,这样客户端拿到克隆结果后可以继续用抽象引用持有它;
  • 这里刻意把克隆方法定义成"约定"而非固定算法——每个具体原型"如何复制自己"只有它自己最清楚,这正是"复制逻辑归对象自身所有"这一思想的体现。

3.2 具体原型:实现复制自身的能力

public class ConcretePrototype extends Prototype {

    private String filed;

    public ConcretePrototype(String filed) {
        this.filed = filed;
    }

    @Override
    Prototype myClone() {
        return new ConcretePrototype(filed);
    }

    @Override
    public String toString() {
        return filed;
    }
}

要点拆解:

  • 具体原型通过构造器接收并保存自己的状态字段 filed
  • myClone() 的实现方式在此例中为"new 一个新对象,并把原型的状态 filed 传给新对象"。这是在 Java 中实现自复制最直白、最可控的做法:克隆权由对象自己行使,天然拥有访问自身私有字段的能力;
  • 覆盖 toString() 是为了在客户端输出时能直观看到对象携带的状态,便于验证克隆结果;
  • 注意这里复制的是"按引用"还是"按值"取决于字段类型:String 在 Java 中是不可变对象,这种赋值天然安全;但如果字段是可变引用类型,就需要注意浅拷贝与深拷贝问题(见第五节)。

3.3 客户端:面向抽象使用克隆

public class Client {
    public static void main(String[] args) {
        Prototype prototype = new ConcretePrototype("abc");
        Prototype clone = prototype.myClone();
        System.out.println(clone.toString());
    }
}

执行流程分析:

  1. 客户端先用 new ConcretePrototype("abc") 完成一次真正的构造,得到一个状态为 "abc" 的原型;
  2. 变量 prototype 声明为抽象的 Prototype 类型,客户端不再关心它背后到底是哪个具体子类;
  3. 调用 prototype.myClone() 获得一个全新的副本对象 clone
  4. 打印 clone 的字符串表示。

程序输出为:

abc

该输出验证了两件事:克隆产物确实被创建,且完整继承了原型的状态 "abc"。同时需要理解,prototypeclone 是两个不同的对象实例——克隆不是引用拷贝,而是对象级别的复制,修改其中一个对象的状态不会(在深拷贝前提下)影响另一个。

3.4 本实现与 GoF 经典形态的差异说明

GoF 原版原型模式的抽象原型通常定义一个 Clone() 方法,且往往与 Cloneable 标记接口配套使用。CS-Notes 此例出于教学简洁性,自定义了 myClone() 抽象方法,语义等价而规避了 JDK Object.clone() 的诸多约束。两种写法的取舍将在下一节结合 JDK 机制展开。

四、JDK 中的克隆机制:Object.clone() 与 Cloneable

设计模式 - 原型模式 一讲在 JDK 一节中指出原型模式在 Java 标准库中的体现是:

理解 Object.clone() 是掌握 Java 原生原型模式的关键,它也是面试中高频辨析点。下面是其契约的准确概括:

4.1 clone() 的两条硬性规则

  1. 必须实现 Cloneable 标记接口Object.clone() 在执行时会检查对象所属类是否实现了 java.lang.Cloneable。如果未实现,会抛出 CloneNotSupportedExceptionCloneable 本身没有任何方法,它纯粹是一个"允许被克隆"的许可标记(Marker Interface);
  2. 访问级别是 protectedObject.clone()protected native 方法,因此外部代码不能直接对任意对象调用 clone(),只有实现了克隆的类自己在覆盖时把访问级别提升为 public,外界才可能调用。

4.2 标准 JDK 克隆写法

public class ConcretePrototype implements Cloneable {

    private String filed;

    public ConcretePrototype(String filed) {
        this.filed = filed;
    }

    @Override
    public ConcretePrototype clone() throws CloneNotSupportedException {
        return (ConcretePrototype) super.clone();
    }
}

两点关键辨析:

  • super.clone() 的行为Object.clone() 是一个 native 方法,它不调用任何构造函数,而是直接在内存层面按位复制对象,再为目标对象分配新内存。因此克隆得到的对象不会经过构造函数——这一点与第三节 myClone() 内部使用 new 的实现有本质区别;
  • 返回类型可协变:覆盖时可以把返回类型收窄为 ConcretePrototype,JDK 5 之后支持协变返回类型,客户端因此无需再强转。

4.3 为什么 JDK 中的"原版"这么绕

从 GoF 模式视角看,Object.clone() + Cloneable 恰恰构成了一套内建的原型机制:任何类都可以作为"原型",通过克隆产出新对象,客户端只需依赖 Cloneable 抽象能力。但它的两大限制——必须显式标记、且默认是浅拷贝(shallow copy),决定了工程实践中往往需要开发者自定义克隆逻辑(类似 CS-Notes 示例中的 myClone()),这正是原文档刻意演示自定义克隆方法的原因。

五、浅拷贝与深拷贝:原型模式最核心的工程陷阱

当原型对象内部持有可变引用类型字段时,"克隆"二字就需要回答一个本质问题:新对象与原对象要共享内部对象,还是各自持有一份独立的内部对象? 这决定了克隆是浅拷贝还是深拷贝。

5.1 浅拷贝:共享内部引用

public class Address {
    String city;
    Address(String city) { this.city = city; }
}

public class Employee implements Cloneable {
    String name;
    Address address;   // 可变引用类型字段

    Employee(String name, Address address) {
        this.name = name;
        this.address = address;
    }

    @Override
    public Employee clone() {
        try {
            return (Employee) super.clone();   // 默认浅拷贝
        } catch (CloneNotSupportedException e) {
            throw new AssertionError(e);
        }
    }
}

此时 clone() 得到的新 Employee 与原型共享同一个 Address 对象。如果通过副本修改 address.city,原型的 address 也会跟着变化。浅拷贝适合内部字段全为不可变对象、或明确需要共享内部状态的场景,否则极易埋下隐患。

5.2 深拷贝:递归复制内部对象

@Override
public Employee clone() {
    try {
        Employee copy = (Employee) super.clone();
        copy.address = address.clone();   // 对每个可变引用字段逐一克隆
        return copy;
    } catch (CloneNotSupportedException e) {
        throw new AssertionError(e);
    }
}

深拷贝要求 Address 自身也支持 clone(),然后在新对象中把引用字段替换为独立副本,实现"原型与副本互不影响"。字段数量越多、嵌套层次越深,手动深拷贝越繁琐——这也是工程中常借助序列化、JSON 转换或专门工具库实现深拷贝的现实原因。

5.3 回顾 CS-Notes 示例为何无需区分

原文档 ConcretePrototype 唯一的字段是 StringString 在 Java 中不可变,无论复制时是共享还是重建,都不会出现"改一个影响另一个"的问题,因此示例可以直接用 new ConcretePrototype(filed) 完成安全复制。原型模式本身不规定拷贝深度,深浅与否完全由具体原型类的实现决定,这一点是实现者需要时刻自省的设计决策。

六、原型模式的应用价值与辨析边界

6.1 适用场景

结合原文档的意图与上述原理,原型模式通常应用于以下场景:

  1. 对象创建成本高:对象初始化涉及数据库查询、文件加载、网络请求、复杂计算等昂贵过程,而运行时又需要多个状态相似的对象;
  2. 运行时才确定创建哪种对象:在动态加载或反射场景下,客户端事先不知道具体类名,但已持有一个实例,可通过克隆按需生成;
  3. 需要保留对象的某一时刻快照:例如编辑器中的撤销/恢复、配置的默认值模板复制等;
  4. 希望消除大量相似构造代码:用克隆统一替代繁琐的逐字段 setter 赋值。

6.2 原型模式与直接 new 的辨析

维度 直接 new 原型克隆
构造过程 每次都完整执行构造逻辑 只构造一次,之后按内存/自定义逻辑复制
对构造细节的依赖 客户端需要知道具体类及参数 客户端面向抽象原型,无需了解构造细节
初始状态 每次都要手动逐项设置 直接继承原型已就绪的状态
对象独立性 天然独立 需要自行保证拷贝深度(浅/深)

6.3 原型模式与工厂模式的辨析

两者同属创建型模式,但出发点不同:

  • 工厂模式把"创建哪一类对象"的决定权集中到工厂,本质仍是 new,客户端通过工厂方法获得对象;
  • 原型模式把"如何复制自己"的逻辑交给对象自身,本质上绕开了 new 的常规构造路径,尤其适合"拷贝现成状态"而非"从头构建"的场景。

当对象构造参数复杂且子类众多时,工厂往往需要为每个子类准备分支;原型则天然支持多态克隆,客户端持有的原型引用在调用克隆时动态分派到对应子类。这与仓库中同属创建型的 简单工厂、工厂方法、抽象工厂、生成器(Builder) 等模式可以互为对照,形成完整的创建型问题解决图谱(见 设计模式目录 与 设计模式总览)。

七、使用注意事项与总结

7.1 实践要点清单

  1. 抽象原型必须定义统一的克隆接口,让客户端面向抽象编程(对应原文档抽象类 Prototype);
  2. 每个具体原型自述克隆逻辑,只有它自己最清楚自身字段如何复制;
  3. 明确拷贝深度:字段含可变引用时,按业务需要实现浅拷贝或深拷贝,并写清约定;
  4. 选择克隆实现方式:自定义克隆方法(如原文档的 new ConcretePrototype(filed))简单直观、可完全控制;基于 Object.clone() 的写法需注意 Cloneable 标记、CloneNotSupportedException 处理与"不经过构造器"的语义差异;
  5. 警惕 clone 与 final 字段、单例模式的冲突Object.clone() 不调用构造器,复制过程中需要另行处理依赖构造器完成初始化的字段。

7.2 一句话总结

原型模式的精髓在于:把"制造新对象"的责任从客户端与构造器身上剥离,交给原型对象自身的克隆能力,从而实现状态复用、构造降本与客户端解耦。围绕 设计模式 - 原型模式 展开的意图、类图、实现与 JDK 四要素,构成了理解这一模式并迁移到真实工程的最小完整知识闭环。

延伸阅读:仓库内完整梳理了 GoF 全部模式,可按创建型(单例、简单工厂、工厂方法、抽象工厂、生成器、原型)、行为型、结构型分类继续研读 设计模式目录,并配合汇总文档 设计模式.md 中的目录跳转快速定位任一模式的 Intent、Class Diagram、Implementation 与 JDK 章节。

登录后查看全文
热门项目推荐
相关项目推荐