本文是【GoF设计模式】系列第5篇
前言

为什么需要原型模式?
有些场景下,创建一个对象不是简单的 new 就能搞定的------可能要查数据库、调接口、做复杂计算,初始化过程很重。更麻烦的是,有时需要创建大量相似对象,每个只有细微差别:
java
// 场景:创建一个游戏角色,和已有的角色几乎一样,只是名字不同
GameCharacter warriorTemplate = loadFromDatabase("warrior"); // 查装备、技能、属性,很耗时
// 如果要创建 100 个相似角色,每次重新查数据库?
这种时候,"复制粘贴"比"从零开始"高效得多,可以使用原型模式。
概念
原型模式(Prototype Pattern)是创建型 设计模式,核心思想:通过复制现有对象来创建新对象,跳过复杂的初始化逻辑(如数据库查询、网络请求、复杂计算等),而非每次从零开始构造。
原型模式包含三个核心角色:
- Prototype(抽象原型) :声明克隆方法
clone() - ConcretePrototype(具体原型):实现克隆方法,复制自身并返回新对象
- PrototypeRegistry(原型注册表):存储和检索常用原型,客户端通过 key 获取克隆副本
实现
管理
使用
调用 clone()
<<interface>>
Prototype
+clone() : Prototype
ConcretePrototype
-field String
+clone() : Prototype
PrototypeRegistry
-prototypes Map<String, Prototype>
+register(key, prototype) : void
+get(key) : Prototype
Client
Prototype 定接口,ConcretePrototype 实现拷贝,PrototypeRegistry 管理缓存,Client 通过注册表拿克隆。
原型模式的核心在于拷贝方式的选择:
- 浅拷贝(Shallow Copy):只复制基本类型字段,引用类型字段复制的是引用地址,新旧对象共享同一个引用对象
- 深拷贝(Deep Copy):基本类型和引用类型都完全复制,新旧对象完全独立
java
// 浅拷贝:两个对象共享同一个 List
class ShallowPrototype {
private String name;
private List<String> tags;
@Override
public ShallowPrototype clone() {
ShallowPrototype copy = new ShallowPrototype();
copy.name = this.name;
copy.tags = this.tags; // 共享引用,改一个影响另一个
return copy;
}
}
// 深拷贝:每个对象有独立的 List
class DeepPrototype {
private String name;
private List<String> tags;
@Override
public DeepPrototype clone() {
DeepPrototype copy = new DeepPrototype();
copy.name = this.name;
copy.tags = new ArrayList<>(this.tags); // 独立拷贝
return copy;
}
}
原型模式解决的是"跳过昂贵构造"的问题。深拷贝还是浅拷贝,取决于引用字段是否需要独立------选错了,要么浪费性能,要么引入共享 bug。
实现
GoF 标准实现
GoF 原型模式的核心是定义 Prototype 接口,具体原型类实现 clone() 方法,再配合原型注册表统一管理原型对象。
java
// 抽象原型接口
public interface Prototype {
public Prototype clone();
}
// 具体原型类
public class ConcretePrototype implements Prototype {
private String data;
private List<String> items;
public ConcretePrototype(String data, List<String> items) {
this.data = data;
this.items = items;
}
@Override
public Prototype clone() {
// 深拷贝:引用类型也要复制
return new ConcretePrototype(this.data, new ArrayList<>(this.items));
}
}
// 原型注册表:存储和检索原型对象
public class PrototypeRegistry {
private Map<String, Prototype> prototypes = new HashMap<>();
public void register(String key, Prototype prototype) {
prototypes.put(key, prototype);
}
public Prototype get(String key) {
Prototype prototype = prototypes.get(key);
if (prototype == null) {
throw new IllegalArgumentException("未注册的原型: " + key);
}
return prototype.clone(); // 返回克隆副本,保护原型不被外部修改
}
}
// 客户端:通过注册表获取克隆,不依赖具体类型
PrototypeRegistry registry = new PrototypeRegistry();
registry.register("default", new ConcretePrototype("hello", Arrays.asList("a", "b")));
Prototype copy = registry.get("default"); // 拿到的是全新副本
注册表让客户端不直接持有原型,通过 key 获取克隆副本,既保护了原型不被破坏,又将"用什么原型"的决策集中管理。
拷贝构造器
拷贝构造器是原型模式的另一种常见实现方式------定义一个构造器,接收同类型的对象作为参数,逐字段复制。它比 Cloneable 更直观、类型安全,不需要强转。
java
public class Document {
private String title;
private String content;
private List<String> tags;
private Date createdAt;
// 普通构造器
public Document(String title, String content) {
this.title = title;
this.content = content;
this.tags = new ArrayList<>();
this.createdAt = new Date();
}
// 拷贝构造器:接收同类型对象,逐字段深拷贝
public Document(Document other) {
this.title = other.title;
this.content = other.content;
this.tags = new ArrayList<>(other.tags); // 深拷贝引用类型
this.createdAt = new Date(other.createdAt.getTime()); // Date 可变,也要深拷贝
}
// 对外暴露的克隆入口
public Document copy() {
return new Document(this);
}
}
// 使用
Document original = new Document("设计文档", "原型模式...");
Document copy = original.copy();
拷贝构造器 vs Cloneable:
- 拷贝构造器不依赖
Object.clone(),不要求实现标记接口,编译期就能检查类型,没有强转和异常处理 Cloneable的优势是super.clone()是 native 方法,对字段多的对象性能更好
实际开发中,拷贝构造器更推荐 ------代码清晰、类型安全、不需要 catch 异常。"不实现 Cloneable 就抛异常"的问题在拷贝构造器中完全不存在。
回到 GoF 标准实现的代码:
clone()内部就是new ConcretePrototype(this.data, new ArrayList<>(this.items))------本质上就是拷贝构造器的思路,只是包在了clone()方法里。
Java Cloneable 接口
Java 提供了 Cloneable 接口和 Object.clone() 方法作为内置实现。Object.clone() 是 native 方法(由 JVM 底层 C++ 实现,绕过 Java 层面的字段逐个赋值),性能最好,默认执行浅拷贝。
Cloneable 的设计缺陷 :
Cloneable是标记接口(没有方法),clone()却定义在Object上,而且是protected的。导致两个问题:1)不实现Cloneable就调clone()会抛异常,但编译器不会提示;2)外部类无法直接调用,必须手动 override 并改为 public。
java
public class Rectangle implements Cloneable {
private String color;
private int width;
private int height;
private List<String> tags; // 引用类型
public Rectangle(String color, int width, int height, List<String> tags) {
this.color = color;
this.width = width;
this.height = height;
this.tags = tags;
}
// 浅拷贝:tags 被共享
@Override
public Object clone() {
try {
return super.clone();
} catch (CloneNotSupportedException e) {
throw new RuntimeException(e);
}
}
// 深拷贝版本:tags 独立
public Object deepClone() {
try {
Rectangle copy = (Rectangle) super.clone();
copy.tags = new ArrayList<>(this.tags);
return copy;
} catch (CloneNotSupportedException e) {
throw new RuntimeException(e);
}
}
}
如果不实现 Cloneable 接口就调用 clone(),会抛出 CloneNotSupportedException。适合类结构简单、字段大多是基本类型或不可变类型的场景。
序列化实现深拷贝
对象嵌套层级深、结构复杂时,手动递归克隆代码量大且容易遗漏。序列化是把对象转成字节流,反序列化是把字节流还原成对象------整个过程会递归复制所有引用对象,天然实现深拷贝。
java
import java.io.*;
public class DeepCloneUtil {
@SuppressWarnings("unchecked")
public static <T extends Serializable> T deepClone(T obj) {
try {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(obj);
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis);
return (T) ois.readObject();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
// 使用:所有引用的类都必须实现 Serializable
GameCharacter copy = DeepCloneUtil.deepClone(original);
优点是一行代码搞定复杂嵌套对象,缺点是性能较差(比手动克隆慢 10 倍以上),且所有引用的类必须实现 Serializable。
Java 原生序列化存在已知安全风险(反序列化漏洞),实际项目中更推荐 JSON 序列化(Jackson/Gson,不要求
Serializable)、Apache CommonsSerializationUtils.clone()(封装了模板代码)、MapStruct(编译期生成,性能最好)等方案。
如何选择
| 实现方式 | 拷贝类型 | 实现难度 | 性能 | 适用场景 |
|---|---|---|---|---|
| GoF 标准实现 | 自定义(通常深拷贝) | 中等 | 较快 | 面向接口编程,客户端不依赖具体类 |
| 拷贝构造器 | 自定义(通常深拷贝) | 简单 | 较快 | 不需要接口抽象,追求类型安全和可读性 |
| Java Cloneable(浅拷贝) | 浅拷贝 | 简单 | 最快 | 字段都是基本类型或不可变类型 |
| Java Cloneable(深拷贝) | 深拷贝 | 中等 | 较快 | 有可变引用类型字段,结构不深 |
| 序列化深拷贝 | 深拷贝 | 简单 | 较慢 | 结构复杂、嵌套深 |
大多数业务场景用拷贝构造器 或 Cloneable + 手动深拷贝引用字段就够了,实现成本适中、性能好、可控性强。
总结
原型模式解决的是"对象创建成本高"的问题------与其从零构造,不如复制已有对象再微调。
什么时候用:
- 对象创建成本高(涉及数据库查询、网络请求、复杂计算)------克隆跳过昂贵构造
- 需要大量相似对象------克隆后只改差异字段,比逐个 new 快得多
- 运行时动态确定对象状态------编译期不知道具体配置,只能基于已有对象拷贝
- 需要保存和恢复对象状态------存档/撤销功能,克隆快照再恢复
什么时候不用:
- 对象创建成本低------直接 new 更简单直观
- 对象字段简单且不可变------拷贝意义不大
- 类层次复杂,每个子类都要实现 clone------维护成本高
简单记忆:
原型是"复制粘贴",工厂是"按图纸生产"。创建成本低用工厂,创建成本高用原型。
原型模式 vs 工厂方法模式:
| 维度 | 原型模式 | 工厂方法 |
|---|---|---|
| 核心意图 | 通过克隆已有对象来创建新对象 | 通过子类决定创建哪个类的对象 |
| 创建方式 | 拷贝(clone) | 实例化(new) |
| 结构差异 | 不需要子类,原型自身负责克隆 | 需要 Creator 子类来决定 Product |
| 关注点 | 跳过昂贵的构造过程 | 让子类决定实例化哪个类 |
| 典型场景 | 对象创建成本高、需要大量相似对象 | 需要根据条件创建不同类型的对象 |
逐步区分法:
- 先看对象创建成本------成本高(查数据库、复杂计算)→ 原型模式
- 再看是否需要根据类型/条件创建不同对象 → 工厂方法
- 两者都需要?→ 可以组合:工厂方法内部用原型克隆来创建对象
大多数场景下,如果只是创建对象,工厂方法更直观、更常用。只有当对象创建成本确实高、或者需要大量相似对象时,才值得用原型模式。
练习题目
游戏角色模板克隆
题目描述 :
某游戏公司开发角色创建系统,系统中有角色模板(原型角色),包含角色名称、生命值和技能列表。新角色通过克隆模板创建,创建后可以修改名称、生命值,并添加新技能。请你使用原型模式实现角色克隆功能,确保克隆出的角色修改后不会影响原型角色。
输入描述 :
第一行输入原型角色的信息:名称、生命值、技能列表(技能之间用英文逗号分隔,如 Attack,Defense)
第二行输入整数 N(1 ≤ N ≤ 10),表示需要克隆的角色数量
接下来 N 行,每行输入克隆角色的修改信息:新名称、新生命值、新增技能
输出描述 :
首先输出原型角色的信息,然后输出每个克隆角色的信息,最后再次输出原型角色的信息(用于验证原型未被修改)。每行格式:Name: {name}, HP: {hp}, Skills: {skills},其中 skills 按列表格式输出。
输入示例:
Warrior 100 Attack,Defense
3
Knight1 80 Shield
Knight2 90 Fireball
Knight3 75 Heal
输出示例:
--- Prototype ---
Name: Warrior, HP: 100, Skills: [Attack, Defense]
--- Clones ---
Name: Knight1, HP: 80, Skills: [Attack, Defense, Shield]
Name: Knight2, HP: 90, Skills: [Attack, Defense, Fireball]
Name: Knight3, HP: 75, Skills: [Attack, Defense, Heal]
--- Prototype After Cloning ---
Name: Warrior, HP: 100, Skills: [Attack, Defense]
解题思路:
- 角色识别 :
CharacterPrototype是抽象原型接口(声明 clone),Character是具体原型类(实现 clone + 深拷贝技能列表) - 核心陷阱:技能列表是引用类型,如果只做浅拷贝,修改克隆对象的技能会污染原型------这就是题目最后验证原型未被修改的原因
- 安全保障:不仅 clone 中要深拷贝,构造函数也要深拷贝传入的列表,防止外部修改影响内部
java
import java.util.*;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
// 读取原型角色
String protoName = sc.next();
int protoHP = sc.nextInt();
List<String> protoSkills = new ArrayList<>(Arrays.asList(sc.next().split(",")));
Character prototype = new Character(protoName, protoHP, protoSkills);
System.out.println("--- Prototype ---");
prototype.show();
// 读取克隆数量并逐个克隆
int n = sc.nextInt();
System.out.println("--- Clones ---");
for (int i = 0; i < n; i++) {
String newName = sc.next();
int newHP = sc.nextInt();
String newSkill = sc.next();
Character clone = prototype.clone();
clone.setName(newName);
clone.setHP(newHP);
clone.addSkill(newSkill);
clone.show();
}
System.out.println("--- Prototype After Cloning ---");
prototype.show();
}
}
// 抽象原型接口
interface CharacterPrototype {
public CharacterPrototype clone();
}
// 具体原型类
class Character implements CharacterPrototype {
private String name;
private int hp;
private List<String> skills;
public Character(String name, int hp, List<String> skills) {
this.name = name;
this.hp = hp;
this.skills = new ArrayList<>(skills); // 防御性拷贝
}
@Override
public Character clone() {
// 关键:skills 是引用类型,必须深拷贝
return new Character(this.name, this.hp, this.skills);
}
public void setName(String name) { this.name = name; }
public void setHP(int hp) { this.hp = hp; }
public void addSkill(String skill) { this.skills.add(skill); }
public void show() {
System.out.println("Name: " + this.name + ", HP: " + this.hp + ", Skills: " + this.skills);
}
}
扩展:实际项目中的原型模式
Spring 框架的原型 Bean
Spring 的 Bean 默认是单例,但通过 @Scope("prototype") 可以声明为原型作用域------每次 getBean() 返回新实例。
java
@Component
@Scope("prototype")
public class UserRequest {
private String userId;
private Map<String, Object> params;
public UserRequest() {
this.params = new HashMap<>();
}
public void setUserId(String userId) { this.userId = userId; }
public void addParam(String key, Object value) { this.params.put(key, value); }
}
// 使用
@Autowired
private ApplicationContext context;
UserRequest req1 = context.getBean(UserRequest.class);
req1.setUserId("user1");
UserRequest req2 = context.getBean(UserRequest.class);
req2.setUserId("user2");
// req1 != req2,两个请求完全独立
注意 Spring 的 prototype scope 不是 调用 clone(),而是每次 getBean() 都重新执行创建流程(构造器 + 依赖注入 + 初始化回调)。它体现的是原型模式的思想------客户端通过"原型定义"获取新实例,不需要知道创建细节,但实现机制与 GoF 原型模式不同。
消息模板系统
消息推送系统中,不同类型消息有各自的模板。模板只创建一次(加载配置、注册渠道),后续通过克隆快速生成实例。
java
public abstract class MessageTemplate implements Cloneable {
protected String title;
protected String content;
protected String channel;
protected Map<String, String> extra;
public MessageTemplate(String title, String content, String channel) {
this.title = title;
this.content = content;
this.channel = channel;
this.extra = new HashMap<>();
}
@Override
public Object clone() {
try {
MessageTemplate copy = (MessageTemplate) super.clone();
copy.extra = new HashMap<>(this.extra); // Map 必须深拷贝
return copy;
} catch (CloneNotSupportedException e) {
throw new RuntimeException(e);
}
}
public abstract void render(Map<String, Object> params);
}
public class OrderNotifyTemplate extends MessageTemplate {
public OrderNotifyTemplate() {
super("订单通知", "您有一笔新订单待处理", "push");
}
@Override
public void render(Map<String, Object> params) {
this.content = String.format("订单号:%s,金额:%s 元",
params.get("orderId"), params.get("amount"));
}
}
// 使用:模板只创建一次,后续克隆
OrderNotifyTemplate template = new OrderNotifyTemplate();
MessageTemplate msg1 = (MessageTemplate) template.clone();
msg1.render(Map.of("orderId", "ORD001", "amount", "99.9"));
MessageTemplate msg2 = (MessageTemplate) template.clone();
msg2.render(Map.of("orderId", "ORD002", "amount", "199.0"));
关键:extra 是 Map 引用类型,clone 中必须深拷贝,否则所有克隆实例共享同一个 Map,改一个全乱。
游戏中生成相似敌人
同一类型敌人有相同的属性模板(动画、AI 配置等),但每个实例需要独立的位置和当前状态。配合原型注册表管理多种敌人模板。
java
public class Enemy implements Cloneable {
private String type;
private int baseHp;
private int baseAttack;
private List<String> skills;
private int currentHp;
private int x, y;
public Enemy(String type, int baseHp, int baseAttack, List<String> skills) {
this.type = type;
this.baseHp = baseHp;
this.baseAttack = baseAttack;
this.skills = skills;
}
@Override
public Enemy clone() {
try {
Enemy copy = (Enemy) super.clone();
copy.skills = new ArrayList<>(this.skills);
return copy;
} catch (CloneNotSupportedException e) {
throw new RuntimeException(e);
}
}
public void spawn(int x, int y) {
this.currentHp = this.baseHp;
this.x = x;
this.y = y;
}
}
// 原型注册表:统一管理敌人模板
class EnemyRegistry {
private Map<String, Enemy> prototypes = new HashMap<>();
public void register(String key, Enemy prototype) {
prototypes.put(key, prototype);
}
public Enemy create(String key) {
Enemy prototype = prototypes.get(key);
if (prototype == null) {
throw new IllegalArgumentException("未注册的敌人类型: " + key);
}
return prototype.clone();
}
}
// 游戏初始化:注册原型(涉及加载资源、AI 配置等昂贵操作)
EnemyRegistry registry = new EnemyRegistry();
registry.register("goblin", new Enemy("哥布林", 100, 15, Arrays.asList("砍击", "逃跑")));
registry.register("orc", new Enemy("兽人", 200, 25, Arrays.asList("重击", "冲锋")));
// 战斗中快速生成:通过 key 获取克隆,无需关心创建细节
for (int i = 0; i < 10; i++) {
Enemy goblin = registry.create("goblin");
goblin.spawn(randomX(), randomY());
enemies.add(goblin);
}
注册表的好处:敌人模板种类多了之后,客户端不需要自己持有各种原型引用,通过 key 获取即可,集中管理。
配置对象复制
微服务中,不同环境(开发、测试、生产)需要基于同一份基础配置做局部修改。
java
public class ServiceConfig implements Cloneable {
private String serviceName;
private int port;
private int timeout;
private List<String> allowedOrigins;
private Map<String, String> customHeaders;
public ServiceConfig(String serviceName, int port, int timeout) {
this.serviceName = serviceName;
this.port = port;
this.timeout = timeout;
this.allowedOrigins = new ArrayList<>();
this.customHeaders = new HashMap<>();
}
@Override
public ServiceConfig clone() {
try {
ServiceConfig copy = (ServiceConfig) super.clone();
copy.allowedOrigins = new ArrayList<>(this.allowedOrigins);
copy.customHeaders = new HashMap<>(this.customHeaders);
return copy;
} catch (CloneNotSupportedException e) {
throw new RuntimeException(e);
}
}
public ServiceConfig setPort(int port) { this.port = port; return this; }
public ServiceConfig setTimeout(int timeout) { this.timeout = timeout; return this; }
public ServiceConfig addOrigin(String origin) { this.allowedOrigins.add(origin); return this; }
}
// 基础配置定义一次,各环境克隆后按需修改
ServiceConfig baseConfig = new ServiceConfig("order-service", 8080, 3000);
baseConfig.addOrigin("https://www.example.com");
ServiceConfig prodConfig = baseConfig.clone();
prodConfig.setTimeout(5000).addOrigin("https://api.example.com");
ServiceConfig testConfig = baseConfig.clone();
testConfig.setPort(9090).setTimeout(60000);
关键:allowedOrigins 和 customHeaders 都是可变引用类型,clone 中必须深拷贝,否则修改一个环境的配置会影响其他环境。
文档编辑器的元素复制
富文本编辑器中,用户复制段落需要创建完全独立的副本,修改不影响原文档。
java
public class TextBlock implements Cloneable {
private String text;
private String fontFamily;
private int fontSize;
private String color;
private String alignment;
public TextBlock(String text) {
this.text = text;
this.fontFamily = "Arial";
this.fontSize = 14;
this.color = "#000000";
this.alignment = "left";
}
@Override
public TextBlock clone() {
try {
return (TextBlock) super.clone();
} catch (CloneNotSupportedException e) {
throw new RuntimeException(e);
}
}
public void setText(String text) { this.text = text; }
}
// 用户复制段落
TextBlock original = document.getBlock(5);
TextBlock copy = original.clone();
copy.setText(original.getText() + " (副本)");
document.addBlock(copy);
TextBlock 所有字段都是基本类型或不可变类型(String),浅拷贝就够了。但注意:如果后续增加了可变引用类型字段(如嵌套的图片列表),必须升级为深拷贝------这是使用原型模式最容易踩的坑。
现在可能还用不到原型模式,但等碰到"对象创建要查数据库、要调接口、初始化很重"的场景时,与其重新走一遍构造流程,不如克隆一份再微调------这就是原型模式的价值。