CS-Notes 设计模式解读:原型模式(Prototype)的原理、实现与 JDK 原型应用
原型模式(Prototype Pattern)是创建型设计模式之一,它通过"复制一个已有实例(原型)"来产生新对象,而不是通过 new 重新构造。本文以 CS-Notes 仓库中 设计模式 - 原型模式 一讲为核心骨架,结合 Java 语言特性、Object.clone() 与 Cloneable 契约,以及浅拷贝与深拷贝的边界问题,讲解原型模式如何做到"以复制代替构造",并给出可直接运行、可复制的完整代码示例与面试级辨析结论。读完本文,你将能够独立完成原型模式的 Java 实现,理解 JDK 内建克隆机制的限制,并在合适的业务场景中正确选用原型模式。
一、模式意图:用复制代替构造
使用原型实例指定要创建对象的类型,通过复制这个原型来创建新对象。
这是 设计模式 - 原型模式 中对该模式意图的原始定义,也是理解整个模式的一把钥匙。它包含两个关键动作:
- 先用一次完整的构造过程创建出一个"样板"对象,即原型实例;
- 此后所有新对象不再走构造函数,而是调用原型对象的克隆方法复制生成。
在 Java 这类语言中,new 触发构造意味着必须经历完整的构造逻辑,而原型对象可能是一个已经被填充了大量默认状态、配置完成、甚至经过昂贵初始化的对象。原型模式把"构造"与"复制"解耦:构造只发生一次,复制可以发生无数次,从而在对象创建成本高、或创建参数复杂时降低开销,同时把对象的创建过程封装在对象自身内部。
需要特别强调的是,原型模式解决的核心矛盾是 "客户端面对抽象类型,却需要按已有实例的状态批量生成新实例"。客户端只依赖抽象原型类型,并不知道也不需要知道具体子类的构造细节。
二、类图:四个角色的职责划分
原型模式的标准类图如下(原文档图示):
从类图可以看出原型模式由四类角色构成,分别对应原文档的 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()直接冲突(后者受protected与Cloneable标记双重限制,详见第四节); - 返回类型是抽象类型
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());
}
}
执行流程分析:
- 客户端先用
new ConcretePrototype("abc")完成一次真正的构造,得到一个状态为"abc"的原型; - 变量
prototype声明为抽象的Prototype类型,客户端不再关心它背后到底是哪个具体子类; - 调用
prototype.myClone()获得一个全新的副本对象clone; - 打印
clone的字符串表示。
程序输出为:
abc
该输出验证了两件事:克隆产物确实被创建,且完整继承了原型的状态 "abc"。同时需要理解,prototype 与 clone 是两个不同的对象实例——克隆不是引用拷贝,而是对象级别的复制,修改其中一个对象的状态不会(在深拷贝前提下)影响另一个。
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() 的两条硬性规则
- 必须实现
Cloneable标记接口:Object.clone()在执行时会检查对象所属类是否实现了java.lang.Cloneable。如果未实现,会抛出CloneNotSupportedException。Cloneable本身没有任何方法,它纯粹是一个"允许被克隆"的许可标记(Marker Interface); - 访问级别是
protected:Object.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 唯一的字段是 String,String 在 Java 中不可变,无论复制时是共享还是重建,都不会出现"改一个影响另一个"的问题,因此示例可以直接用 new ConcretePrototype(filed) 完成安全复制。原型模式本身不规定拷贝深度,深浅与否完全由具体原型类的实现决定,这一点是实现者需要时刻自省的设计决策。
六、原型模式的应用价值与辨析边界
6.1 适用场景
结合原文档的意图与上述原理,原型模式通常应用于以下场景:
- 对象创建成本高:对象初始化涉及数据库查询、文件加载、网络请求、复杂计算等昂贵过程,而运行时又需要多个状态相似的对象;
- 运行时才确定创建哪种对象:在动态加载或反射场景下,客户端事先不知道具体类名,但已持有一个实例,可通过克隆按需生成;
- 需要保留对象的某一时刻快照:例如编辑器中的撤销/恢复、配置的默认值模板复制等;
- 希望消除大量相似构造代码:用克隆统一替代繁琐的逐字段 setter 赋值。
6.2 原型模式与直接 new 的辨析
| 维度 | 直接 new |
原型克隆 |
|---|---|---|
| 构造过程 | 每次都完整执行构造逻辑 | 只构造一次,之后按内存/自定义逻辑复制 |
| 对构造细节的依赖 | 客户端需要知道具体类及参数 | 客户端面向抽象原型,无需了解构造细节 |
| 初始状态 | 每次都要手动逐项设置 | 直接继承原型已就绪的状态 |
| 对象独立性 | 天然独立 | 需要自行保证拷贝深度(浅/深) |
6.3 原型模式与工厂模式的辨析
两者同属创建型模式,但出发点不同:
- 工厂模式把"创建哪一类对象"的决定权集中到工厂,本质仍是
new,客户端通过工厂方法获得对象; - 原型模式把"如何复制自己"的逻辑交给对象自身,本质上绕开了
new的常规构造路径,尤其适合"拷贝现成状态"而非"从头构建"的场景。
当对象构造参数复杂且子类众多时,工厂往往需要为每个子类准备分支;原型则天然支持多态克隆,客户端持有的原型引用在调用克隆时动态分派到对应子类。这与仓库中同属创建型的 简单工厂、工厂方法、抽象工厂、生成器(Builder) 等模式可以互为对照,形成完整的创建型问题解决图谱(见 设计模式目录 与 设计模式总览)。
七、使用注意事项与总结
7.1 实践要点清单
- 抽象原型必须定义统一的克隆接口,让客户端面向抽象编程(对应原文档抽象类
Prototype); - 每个具体原型自述克隆逻辑,只有它自己最清楚自身字段如何复制;
- 明确拷贝深度:字段含可变引用时,按业务需要实现浅拷贝或深拷贝,并写清约定;
- 选择克隆实现方式:自定义克隆方法(如原文档的
new ConcretePrototype(filed))简单直观、可完全控制;基于Object.clone()的写法需注意Cloneable标记、CloneNotSupportedException处理与"不经过构造器"的语义差异; - 警惕 clone 与 final 字段、单例模式的冲突:
Object.clone()不调用构造器,复制过程中需要另行处理依赖构造器完成初始化的字段。
7.2 一句话总结
原型模式的精髓在于:把"制造新对象"的责任从客户端与构造器身上剥离,交给原型对象自身的克隆能力,从而实现状态复用、构造降本与客户端解耦。围绕 设计模式 - 原型模式 展开的意图、类图、实现与 JDK 四要素,构成了理解这一模式并迁移到真实工程的最小完整知识闭环。
延伸阅读:仓库内完整梳理了 GoF 全部模式,可按创建型(单例、简单工厂、工厂方法、抽象工厂、生成器、原型)、行为型、结构型分类继续研读 设计模式目录,并配合汇总文档 设计模式.md 中的目录跳转快速定位任一模式的 Intent、Class Diagram、Implementation 与 JDK 章节。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0626
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
