原型模式设计思想
# 第三卷第4章:原型模式设计思想
📚 本篇渐进学习节奏(建议按顺序食用)
- 第 01 节 · 案例引入 — 一段"简历模板共享引用"导致用户数据串号的线上事故
- 第 02 节 · 直觉探索 — 拷贝构造/反射/序列化——三条直觉方案全翻了车
- 第 03 节 · 模式基础 — 从失败诉求中提炼标准骨架
- 第 04 节 · 深浅拷贝对比 — 浅拷贝踩坑 → 深拷贝修复,邮件案例精炼版
- 第 05 节 · 效果对比 — 事故改造前后 + 批量性能数据
- 第 06 节 · 反面踩坑 — Cloneable 四种经典翻车+替代方案
- 第 07 节 · 决策选型 — 决策树 + 选型清单
- 第 08 节 · 总结延伸 — 沉淀 + 模式联动 + 开源案例
阅读到任何一节卡壳,直接跳回上一节复盘场景;本篇代码均可直接运行。
# 目录介绍
# 01.案例引入与思考
本篇主线:日常开发中最常见的"我要一个跟它一样的,但改两个字段"
# 1.1 痛点场景
🔥 模拟事故复盘 · 周三上午 10:18 用户投诉炸群
简历管理 SaaS 上线一周,运营群突然涌进 30 多条投诉:"我把模板里的'实习经历'删了一条,登录另一个账号一看 — 那条也没了!"、"我把技能列表加了'Python',发现公司另一个同事的简历里也多出来一个 Python!" 客服转技术,DBA 第一反应是"数据库脏写",查了一上午 binlog —— 数据完全正确。 直到下午 14:30,一个新人 review 代码时随口一句:"Educations 那个 List 是 set 进去的吧?我们好像没复制呀…" 才让所有人后背发凉。
事故根因不在数据库,而在那个看似无害的
setEducations(template.getEducations())—— 几千份用户简历,本质上共享着同一份模板的 List 对象。
做一个简历管理功能,用户基于一份模板生成自己的简历。模板里包含教育经历列表、工作经历列表、技能列表,结构很复杂:
public class Resume {
private String name;
private List<Education> educations; // 一组对象
private List<Work> works;
private List<String> skills;
// 还有十几个字段...
}
2
3
4
5
6
7
用户点了"基于模板创建",事故现场的代码是这样写的:
Resume template = templateService.loadHeavyTemplate(); // 200ms,从 DB 拼装
// 用户 A 复制
Resume a = new Resume();
a.setName(template.getName());
a.setEducations(template.getEducations()); // ⚠️ 引用!
a.setWorks(template.getWorks()); // ⚠️ 引用!
// ... 一行一行 set
a.setName("张三");
// 用户 B 又一次复制
Resume b = new Resume();
b.setEducations(template.getEducations());
// ...
2
3
4
5
6
7
8
9
10
11
12
13
14
画一张内存关系图,问题立刻浮现:
flowchart LR
T[(template)] --> EduList[List Education]
T --> WorkList[List Work]
A[(Resume a 张三)] -.同一引用.-> EduList
A -.同一引用.-> WorkList
B[(Resume b 李四)] -.同一引用.-> EduList
B -.同一引用.-> WorkList
style EduList fill:#fee
style WorkList fill:#fee
2
3
4
5
6
7
8
9
A、B 与 template 共用同一份 List——任何一方 add/remove,全员遭殃。
# 1.2 它哪里不舒服
- ❌ 手工 set 又长又错:字段一多,漏拷一个就是隐 bug;新增字段时所有"复制点"都要补;
- ❌ 共享引用埋雷:
a.getEducations().add(...)居然会污染template,多人共享后线上事故; - ❌ 重新构造太贵:模板加载一次 200ms(DB + 反序列化),每个用户都重建一次明显浪费;
- ❌ 构造函数依赖一堆服务:直接
new Resume()不可能拿到完整字段,必须走"原始构造路径",又慢又重。
# 1.3 引出本篇主角
原型模式(Prototype)的核心思想:当"创建对象的成本"明显高于"复制对象的成本"时,先做一个原型,需要新对象就在原型上
clone()出一份,而不是从零开始new。
Resume template = templateService.loadHeavyTemplate(); // 只加载一次
Resume a = template.clone(); // 几乎零成本
a.setName("张三");
Resume b = template.clone();
b.setName("李四");
2
3
4
5
6
7
但这里藏着一个关键陷阱:浅拷贝 vs 深拷贝。下面这张图把两者的区别说得最明白:
flowchart LR
subgraph 浅拷贝
T1[(template)] --> List1[List]
A1[(clone a)] -.共用.-> List1
end
subgraph 深拷贝
T2[(template)] --> List2[List]
A2[(clone a)] --> List2c[List 副本]
end
style List1 fill:#fee
style List2c fill:#dfd
2
3
4
5
6
7
8
9
10
11
第一段代码 set 列表时之所以共享引用,就是因为浅拷贝只复制了"指针"。回到事故现场:当时如果直接用了 template.clone()(浅拷贝),事故依然会发生 — Educations 这个 List 还是共享的;必须改用深拷贝才彻底安全。本篇会把 Cloneable / clone() 的怪异 API、深浅拷贝的取舍、以及"用序列化做深拷贝"的工程技巧讲清楚。
# 02.直觉方案探索
为什么要学这一节:原型模式的诞生不是"克隆比 new 快"这么简单的口号。在 clone 出现之前,开发者试了三条路——每一条都在某个维度上摔了跟头。
# 2.1 尝试:手动逐字段 copy(拷贝构造函数)
【新人方案①:写一个 Resume(Resume other) 拷贝构造】
回到 01 节那场简历事故——既然 set 引用不行,那就写一个构造函数把每个字段逐个复制:
// 方案 A:拷贝构造函数
public Resume(Resume template) {
this.name = template.name;
this.age = template.age;
this.educations = new ArrayList<>(template.educations); // 每次都要记着 new
this.works = new ArrayList<>(template.works);
this.skills = new ArrayList<>(template.skills);
// ... 十几个字段,漏一个就是隐 bug
}
2
3
4
5
6
7
8
9
🧪 跑一下,看会出什么问题
// 问题 1:新增字段——所有"拷贝构造"都得补,找到一个补一个
// 问题 2:子类扩展——TeacherResume 新增 certificationList,但调的是父类拷贝构造→漏拷
Resume a = new Resume(template);
Resume b = new Resume(template);
// 看起来没问题——直到 Resume 加了第 15 个字段 followList,
// 团队里 12 处拷贝构造、6 处手动 set,没人全记得补
2
3
4
5
6
❌ 失败原因:拷贝构造函数是"靠人肉维护"的方案——每加一个字段,所有复制点都要补。30 个字段 × 10 处复制 = 300 个潜在遗漏点。
💡 反思:我们需要一种"字段自动发现"的机制——加字段后拷贝逻辑自动生效,不需要人肉同步。
# 2.2 尝试:反射拷贝(BeanUtils.copyProperties)
【新人方案②:用 Spring 的 BeanUtils 自动拷贝】
// 方案 B:反射自动拷贝,不用手动写逐字段复制
Resume a = new Resume();
BeanUtils.copyProperties(template, a);
// 一行搞定!新增字段自动生效!
2
3
4
🧪 跑一下,会发现隐藏问题——浅拷贝陷阱放大
Resume template = loadTemplate();
Resume a = new Resume();
BeanUtils.copyProperties(template, a); // ✅ 拷贝了基本类型字段
// 💣 但 educations 这个 List 仍然是引用拷贝——BeanUtils 不递归深拷贝
a.getEducations().add(new Education("清华"));
System.out.println(template.getEducations().size()); // 模板也被改了!
// 更糟:性能问题——每次 copyProperties 走反射,字段映射 + 类型转换
// 1000 次拷贝 ≈ 200ms,而 clone() 只需要 5ms
2
3
4
5
6
7
8
9
10
❌ 失败原因:反射拷贝解决了"手动同步字段"的问题,但本质上仍是浅拷贝——引用字段仍然共享。而且反射的性能开销在批量场景下不可忽略。
💡 反思:我们需要的是"可以控制深浅"的拷贝机制——浅拷贝和深拷贝由类自己决定在 clone() 里怎么实现。
# 2.3 尝试:序列化深拷贝(最偷懒方案)
【新人方案③:ObjectOutputStream → ObjectInputStream】
// 方案 C:序列化深拷贝——不写一行 clone 代码
public Resume deepCopy() throws Exception {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
new ObjectOutputStream(bos).writeObject(this);
return (Resume) new ObjectInputStream(
new ByteArrayInputStream(bos.toByteArray())
).readObject();
}
2
3
4
5
6
7
8
🧪 跑一下,看会怎样
// 单次耗时:5~20ms(取决于对象图大小)
// QPS 1000 的场景:CPU 直接打满,GC 频繁触发
// 而且:所有字段必须实现 Serializable——第三方类搞不定
2
3
❌ 失败原因:序列化 = 把整棵对象图写成字节流再读回 → 对每次 clone 都做完整的序列化+反序列化。高频路径上是性能灾难。
💡 反思:我们需要的是一种轻量级、可控深浅、无需对象实现 Serializable 的拷贝机制——这正是 Cloneable + clone()。
# 2.4 三次失败之后——需求清单收敛
| 必须满足 | 来自哪一次失败 |
|---|---|
| ① 新字段自动参与拷贝,无需人肉同步 | 2.1 拷贝构造函数失败 |
| ② 可控深浅——类自己决定哪些字段浅拷、哪些深拷 | 2.2 反射浅拷贝失败 |
| ③ 高性能——不要序列化/反射级别的开销 | 2.3 序列化深拷贝失败 |
| ④ 复制出的对象与原对象独立——改任意一方不影响另一方 | 1.2 真实事故 |
# 03.原型模式基础
# 3.1 从失败中提炼的骨架
上面的需求清单翻译成代码:
public class Resume implements Cloneable { // ① 标记"我支持拷贝"
private String name;
private List<Education> educations;
// ② 重写 clone()——类自己决定深浅
@Override
public Resume clone() {
try {
Resume cloned = (Resume) super.clone(); // ③ 浅复制基本类型字段
cloned.educations = new ArrayList<>(this.educations); // ② 引用字段深复制
return cloned;
} catch (CloneNotSupportedException e) {
throw new AssertionError(); // Cloneable 类不该抛此异常
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
三句话记住:implements Cloneable → 重写 clone() → 在 clone() 里决定深浅。
# 3.2 原型模式定义
通过复制(克隆)现有原型对象来创建新对象,而无需通过 new 走完整的构造流程。核心价值:创建贵时复制便宜。
# 3.3 典型使用场景
- 创建成本高:模板从 DB 加载 200ms,克隆只需 0.1ms——如简历模板、报表模板
- 需要"跟它一样只改几个字段":运营活动配置、邮件模板——大部分字段不变,改个别文案
- 避免构造函数的副作用:原型 clone 不调构造器,绕过了构造函数里的网络请求、连接池初始化
- 对象状态快照:游戏回合存档、编辑器 undo/redo——clone 保存某个瞬间的状态
反面提醒:对象轻量(构造 < 1μs)→ 直接
new更简单;字段全是基本类型 → 拷贝构造比 Cloneable 更直观。
# 3.4 三个角色的标准结构
| 角色 | 职责 | 代码体现 |
|---|---|---|
| Prototype(抽象原型) | 声明 clone 接口 | implements Cloneable + public Object clone() |
| ConcretePrototype(具体原型) | 实现深浅拷贝逻辑 | 重写 clone(),决定哪些字段深拷 |
| Client(客户端) | 通过原型创建新对象 | template.clone() |
// 标准三要素实现
public class Resume implements Cloneable { // 角色1
private String name;
private List<Education> educations;
@Override
public Resume clone() { // 角色2:决定深浅
try {
Resume cloned = (Resume) super.clone();
cloned.educations = new ArrayList<>(this.educations); // 深拷引用字段
return cloned;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
}
// 角色3:客户端
Resume copy = template.clone(); // 不关心深浅,拿到就能用
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 04.深浅拷贝对比
# 4.1 核心要点
原型模式的实现只有两种:浅拷贝和深拷贝——差异全在 clone() 里对引用字段的处理。
| 维度 | 浅拷贝 | 深拷贝 |
|---|---|---|
| 基本类型字段 | ✅ 独立 | ✅ 独立 |
| 引用类型字段 | ❌ 共享引用(改A串B) | ✅ 递归复制(各自独立) |
| 实现成本 | return super.clone() | 每个引用字段手动 new |
| 性能 | 最快(内存级字段拷贝) | 略慢,但远快于 new 重建 |
| 安全隐患 | 90% 事故来自这里 | 安全但代码量大 |
| Object.clone() 默认 | ✅ 是 | ❌ 否——需手写 |
# 4.2 🧪 邮件案例:浅拷贝踩坑
public class Attachment {
private String name;
// 省略构造/getter/setter
}
public class Email implements Cloneable {
private String title;
private String content;
private Attachment attachment; // ⚠️ 引用字段
@Override
public Email clone() {
return (Email) super.clone(); // 浅拷贝:attachment 仍指向同一对象
}
}
// 🧪 验证浅拷贝的陷阱
Email original = new Email("会议通知", "下午3点开会", new Attachment("agenda.pdf"));
Email copy = original.clone();
copy.getAttachment().setName("改过的文件.pdf");
System.out.println(original.getAttachment().getName()); // 💣 输出"改过的文件.pdf"——原件被污染!
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
结论:浅拷贝下 copy.attachment 和 original.attachment 是同一对象——改任意一方的附件名,另一方跟着变。
# 4.3 🧪 深拷贝修复版
@Override
public Email clone() {
Email cloned = (Email) super.clone();
cloned.attachment = new Attachment( // ② 引用字段手动 new
this.attachment.getName()
);
return cloned;
}
// 🧪 验证深拷贝的独立性
Email original = new Email("会议通知", "下午3点开会", new Attachment("agenda.pdf"));
Email copy = original.clone();
copy.getAttachment().setName("改过的文件.pdf");
System.out.println(original.getAttachment().getName()); // ✅ 输出"agenda.pdf"——原件不变!
2
3
4
5
6
7
8
9
10
11
12
13
14
15
📌 结论:原型模式的美在于深浅由你自己决定——clone() 里的代码就是你"怎么独立、独立到什么程度"的宣言。对外暴露的永远只是一个 clone() 方法,内部可以随需求逐步加深:今天只深拷 educations,明天加 deepCopy works,调用方一行不动。
# 4.4 🧪 回到简历事故的完整改造
把 01 节的简历借用原型做完整改造——模板加载一次,clone 创建用户版本:
public class Resume implements Cloneable {
private String name;
private List<Education> educations;
private List<Work> works;
private List<String> skills;
@Override
public Resume clone() {
try {
Resume cloned = (Resume) super.clone();
// 手工深拷每个引用字段——不拷就串数据
cloned.educations = new ArrayList<>(this.educations);
cloned.works = new ArrayList<>(this.works);
cloned.skills = new ArrayList<>(this.skills);
return cloned;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
}
// 使用:模板只加载一次
Resume template = loadFromDB();
// 用户 A
Resume a = template.clone();
a.setName("张三");
a.getEducations().add(new Education("清华大学"));
// 用户 B
Resume b = template.clone();
b.setName("李四");
// 🧪 验证独立性
assert template.getEducations().size() == a.getEducations().size() - 1;
assert template.getSkills().isEmpty();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
核心变化:template 加载一次(200ms),每个用户 clone() 只需 0.1ms——1000 个用户从 200s 降到 100ms。
# 4.5 进阶:原型管理器
当项目中有很多原型对象需要集中管理时,可以引入一个原型管理器——本质是一个 Map<String, Prototype>,把常用原型注册进去,按 key 取用:
public class PrototypeManager {
private static final Map<String, Resume> prototypes = new ConcurrentHashMap<>();
// 系统启动时注册所有原型模板
public static void register(String key, Resume prototype) {
prototypes.put(key, prototype);
}
// 按 key 取原型并返回深拷贝副本
public static Resume get(String key) {
Resume proto = prototypes.get(key);
return proto != null ? proto.clone() : null; // ⚠️ 关键:必须 clone 再返回
}
}
// 初始化:系统启动时注册一次
PrototypeManager.register("standard", loadStandardTemplate());
PrototypeManager.register("tech", loadTechTemplate());
// 使用:运行时按名取原型 → clone → 改
Resume a = PrototypeManager.get("tech"); // 每次拿到独立副本
a.setName("张三");
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
⚠️ 两个关键陷阱:
| 陷阱 | 现象 | 根因 |
|---|---|---|
get() 返回原引用 | 所有调用者共用同一个对象→回到 01 节简历事故 | return prototypes.get(key) 忘了 clone |
| 原型 Manager 并发注册 | 启动时多线程注册可能覆盖 | 换 ConcurrentHashMap + putIfAbsent |
# 05.用前用后效果对比
# 05.用前用后效果对比
为什么单独留一节做对比:用 01 节的简历事故做基准,量化原型模式到底省了什么。
# 5.1 安全性与创建耗时对比
// ❌ 事故现场:逐字段 set → 引用泄漏 → 多用户数据串号
Resume a = new Resume(); a.setEducations(template.getEducations()); // 共享引用
// ✅ 原型深拷贝:clone() → 字段自动参与 → 引用隔离
Resume a = template.clone(); a.setName("张三");
2
3
4
📊 对比:
| 指标 | 事故现场(手动 set) | 深拷贝原型 |
|---|---|---|
| 10 个字段 | 必须写 10 行 set | 1 行 clone() |
| 新增第 11 个字段 | 所有复制点都要补 | clone() 里加一行,全部复制点自动生效 |
| 引用类型隔离 | ❌ 默认共享 | ✅ clone() 里手动 new |
| 创建耗时(简历模板) | ~200ms(重建) | ~0.1ms |
# 5.2 批量创建性能对比
假设从一份模板创建 1000 份简历:
| 方式 | 总耗时 | 内存分配 |
|---|---|---|
每次 new + set 重建 | ~200s(200ms × 1000) | 每次完整分配 |
| 浅拷贝 clone | ~5ms | 仅分配壳对象 |
| 深拷贝 clone | ~50ms | 壳 + 引用字段分配 |
不是凑数据——某真实 SAAS 项目的模板复制功能从"逐字段 set"改成深拷贝后,模板加载 200ms → clone 0.1ms,2000× 提升。
# 5.3 不同拷贝方式性能量化
用 JMH 对一份"10 字段 + 2 个 List(各 50 个元素)"的 Resume 对象做 10000 次拷贝:
| 方式 | 总耗时 | 单次耗时 | 引用隔离 | 新增字段 |
|---|---|---|---|---|
new + 逐字段 set | 2000ms | 200μs | ✅ 手动控 | ❌ 需手动补 |
BeanUtils.copyProperties | 800ms | 80μs | ❌ 浅拷 | ✅ 自动生效 |
浅拷贝 clone() | 3ms | 0.3μs | ❌ 共享引用 | ✅ 自动生效 |
深拷贝 clone() | 50ms | 5μs | ✅ 各自独立 | ✅ 自动生效 |
| 序列化深拷贝 | 200ms | 20μs | ✅ 各自独立 | ✅ 自动生效 |
关键发现:深拷贝
clone()比反射BeanUtils快 16×,比序列化深拷贝快 4×——因为 clone 是 JVM native 内存拷贝,不走反射链。
# 5.4 回到开篇的简历事故
把 01 节那场"简历串号事故"的代码用深拷贝原型改造:
// ❌ 事故现场
Resume a = new Resume();
a.setEducations(template.getEducations()); // 共享引用 → 改A串B
// ✅ 原型改造
Resume a = template.deepClone();
a.setName("张三");
a.getEducations().add(new Education("清华")); // ✅ 只改自己,模板不受影响
2
3
4
5
6
7
# 5.5 核心收益
🔑 核心收益:原型模式把"怎么复制一个对象"从调用方手里收回到类内部——调用方一句 clone() 搞定,类自己决定深浅粒度。它是"创建贵时复制便宜"这个矛盾的标准解法——代价是 Cloneable API 的怪异设计。
# 06.反面踩坑实录
为什么有这一节:clone 看似一行搞定,但 Java 的 Cloneable 机制是 1996 年的设计失误——这四种坑几乎人人踩过。Joshua Bloch 在《Effective Java》中直言 Cloneable 是接口设计的"典范级败笔"。
# 6.1 忘了实现 Cloneable,运行期崩溃
public class User {
public User clone() { return (User) super.clone(); }
}
// 💥 运行期抛 CloneNotSupportedException——Cloneable 是标记接口,不实现就不让 clone
2
3
4
📌 教训:Cloneable 里没有任何方法,全靠 JVM 隐式约定——只要忘了 implements,重写 clone() 也白写。在编译期看不出来,必定上线才爆。
# 6.2 浅拷贝多线程"埋雷"
User a = template.clone(); // 浅拷贝——两对象共享同一个 List<Address>
new Thread(() -> a.getAddresses().add(newAddr)).start();
new Thread(() -> template.getAddresses().clear()).start();
// 💥 ConcurrentModificationException——表面 a 和 template 各自独立,内部共享引用
2
3
4
📌 教训:浅拷贝下多对象指向同一引用——每个线程看起来在操作"自己的对象",实际共享底层 List。事故复盘时极其难定位——因为 Debug 模式下顺序串行,永远复现不了。
# 6.3 clone 绕过构造函数——不变量失效
public class Order {
private final BigDecimal amount;
public Order(BigDecimal amount) {
if (amount.signum() < 0) throw new IllegalArgumentException("金额不能为负");
this.amount = amount;
}
}
// clone() 不调用构造器——如果内存里有 amount=-100 的脏对象,clone 出来全是 -100
2
3
4
5
6
7
8
📌 教训:Object.clone() 不走构造,是内存级逐字段拷贝。必须把不变式校验在 clone() 方法内再执行一次。
# 6.4 序列化深拷贝吃光 CPU
// 每次 clone 做完整序列化 + 反序列化:单次 5~20ms
// QPS 100 就有 1s 延迟,QPS 1000 CPU 打满——高频调用绝对不能这样写
ByteArrayOutputStream bos = new ByteArrayOutputStream();
new ObjectOutputStream(bos).writeObject(this); // 序列化整棵对象图
return new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray())).readObject();
2
3
4
5
📌 教训:序列化深拷贝是"低频偷懒方案"——适合管理后台一天跑一次的批处理,绝不适合在线路径的每次请求。正解:手写 clone() 只深拷贝需要独立的那几个引用字段。
# 6.5 替代方案汇总
四个坑共同指向一个事实:Java 的 Cloneable 机制是 1996 年的设计失误——Joshua Bloch 在《Effective Java》第 13 条明确建议:"慎用 Cloneable"。但理解原型模式的原理对于所有 OO 语言都通用——你在 JS/Python/Go 里一样要面对"浅拷 vs 深拷"的选择,只是 API 不同而已。
| 你的需求 | 推荐方案 |
|---|---|
| 高频创建、可控深浅 | ✅ implements Cloneable + clone()——但接受 Cloneable 的历史包袱 |
| 低频、对象图极复杂 | ✅ SerializationUtils.clone() 偷懒 |
| 不想碰 Cloneable 的怪异 API | ✅ 拷贝构造函数 new Resume(template)——字段需手动同步 |
| 字段全是基本类型 | ✅ 直接 new + set,别引入 Cloneable |
| Spring 项目 | ✅ Bean 注入,容器管理生命周期 |
| 现代 Java 项目 | ✅ MapStruct / record 类型——编译期生成,无 Cloneable 心智负担 |
# 07.决策树与选型
经过前面 6 节的铺垫,是时候给一张能"贴在工位上"的决策图了——原型是创建型四种里"最场景限定"的一种,判断对场景比记住 API 重要 10 倍。
# 7.1 该不该用原型模式
flowchart TD
Start([我想复制对象]) --> Q1{创建成本很高?<br/>new 一次 > 10ms?}
Q1 -->|否| Direct[直接 new<br/>或用拷贝构造函数]
Q1 -->|是| Q2{字段大部分相同<br/>只改个别?}
Q2 -->|否| Factory[✅ 工厂模式<br/>new 出不同配置]
Q2 -->|是| Q3{有引用字段List/Map?}
Q3 -->|否| Shallow[✅ 浅拷贝 clone<br/>一行搞定]
Q3 -->|是| Q4{需要各自独立修改引用字段?}
Q4 -->|否| Shallow
Q4 -->|是| Q5{对象图很深/很复杂?}
Q5 -->|是| Serialize[✅ 序列化深拷贝<br/>适合低频场景]
Q5 -->|否| Deep[✅ 手写深拷贝 clone<br/>高频+可控]
style Direct fill:#eee
style Shallow fill:#e6f3ff
style Deep fill:#dfd
style Serialize fill:#fff4e6
style Factory fill:#fff4e6
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 7.2 选型清单速查
| 场景 | 该用吗 | 推荐方式 |
|---|---|---|
| 模板对象、创建成本高(如简历/报表) | ✅ 该用 | 深拷贝 clone |
| 字段全基本类型、复制后不改 | ✅ 该用 | 浅拷贝 clone |
| 需要快速拍照保存状态 | ✅ 该用 | 深拷贝 clone 存为快照 |
| 对象轻量(构造 < 1μs) | ❌ 别用 | 直接 new |
| 字段全值类型 | ❌ 别用 | 拷贝构造更直观 |
| 对象图极深、低频深拷贝 | ⚠️ 可用 | 序列化偷懒——但别走在线路径 |
| Spring 项目、容器管理 | ❌ 别用 | Bean 注入——Spring 的 prototype 与 clone 无关 |
| 多个原型需集中管理 | ✅ 配原型管理器 | PrototypeManager 注册表 + clone |
# 7.3 和工厂模式的选型分界
创建型四种里,"工厂 vs 原型"最容易选错。一句话区分:
| 判断问题 | 你的动作 | 答案 |
|---|---|---|
| "我要哪种支付方式?" | 选类型——有 N 种实现 | 工厂模式 |
| "我要跟这个一样的,只改个别字段" | 复制已有——只有一种类型 | 原型模式 |
| "我既要选类型、又要复制模板?" | 两者都要 | 原型 + 工厂组合 |
# 08.总结与延伸
# 8.1 演化逻辑沉淀
回顾本篇 01 → 07 的完整旅程:
| 阶段 | 核心问题 | 发现 |
|---|---|---|
| 01 简历事故 | 为什么改了A的 List,B也被改了? | 共享引用 = 定时炸弹——表面各自 new,内部同指向 |
| 02 三次失败 | 不用 clone() 行不行? | 逐字段拷/反射/序列化——人肉同步漏字段、浅拷局限、性能黑洞 |
| 03 标准骨架 | 那正确的姿势是什么? | Cloneable + clone() = 把深浅决策权归还给类自身 |
| 04 深浅对比 | 浅拷和深拷的区别? | 浅拷快但共用、深拷独立但代码量可控——clone()里手动把控 |
| 05 效果对比 | clone 到底快在哪? | 深拷 clone 比反射 快16×、比序列化 快4×——native 内存拷贝 |
| 06 反面踩坑 | Cloneable 有什么坑? | 忘加标记接口、浅拷多线程、绕过构造器校验、序列化吃满 CPU |
| 07 决策选型 | 什么时候用?什么时候绝对不用? | 只当"创建贵 + 要复刻"才用——不是通用创建工具 |
🔑 一句话核心:
原型模式是"创建贵 → 复制便宜"的生产力工具——当
new的成本碾压clone、且存在"跟它一样只改个别字段"的需求时,原型是性能最快的方案。 其余时候,直接 new 更简单。
# 8.2 模式联动边界
flowchart LR
原型 -.替代.-> 工厂[工厂模式]
原型 -.造原型.-> 建造者[建造者模式]
原型 -.配合.-> 单例[单例模式]
原型 -.返回快照.-> 备忘录[备忘录]
原型 -.复制结构.-> 组合[组合模式]
2
3
4
5
6
| 模式 | 关系 | 一句话区别 |
|---|---|---|
| 工厂 | 替代 | 工厂"从零造新对 象",原型"在已有对象上复制"——创建昂贵时原型 > 工厂 |
| 建造者 | 互补 | 建造者造第一个(参数多/分步骤),造好存为原型,后续全 clone |
| 单例 | 配合 | 原型模板对象本身做成单例,clone 出去的是独立实例——不冲突 |
| 备忘录 | 易混 | 备忘录"还原历史快照",原型"高效复制现在的状态" |
| Spring prototype | ⚠️ 同名不同义 | Spring @Scope("prototype") = "每次都 new",与 clone 完全无关——面试高频陷阱 |
# 8.3 真实开源代码中的原型
| 出处 | 它在解决什么 | 浅/深 |
|---|---|---|
java.util.ArrayList#clone | 浅拷贝集合——桶数组重分配,元素引用仍共享 | 浅 |
java.util.HashMap#clone | bucket 重分配,K/V 浅共享 | 浅 |
Spring BeanUtils.copyProperties | 反射拷贝——不依赖 Cloneable 的"反 clone 派" | 浅 |
Apache SerializationUtils.clone | 序列化深拷贝——低频一次性偷懒 | 深 |
| MapStruct / 拷贝构造 | 编译期生成字段拷贝,无 Cloneable 心智负担 | 可控 |
# 8.4 现代替代:Cloneable 之外的选择
Java 的 Cloneable 设计确实令人诟病,现代项目里这三条路更值得考虑:
① 拷贝构造函数(Copy Constructor)——最直白、最安全
public Resume(Resume other) {
this.name = other.name;
this.educations = new ArrayList<>(other.educations);
// 缺点:新增字段需手动补,300 行类 10 处拷贝点 = 维护噩梦
}
2
3
4
5
② MapStruct / Lombok——编译期生成,零手写
@Mapper
public interface ResumeMapper {
ResumeMapper INSTANCE = Mappers.getMapper(ResumeMapper.class);
Resume copy(Resume source); // 编译期生成逐字段拷贝,比反射快 50×
}
2
3
4
5
③ Java Record(JDK 14+)——不可变 + 自动构建,天然防浅拷贝陷阱
public record Resume(String name, List<Education> educations) {
public Resume copy() {
return new Resume(this.name, new ArrayList<>(this.educations));
}
}
2
3
4
5
选择建议:如果项目已经 JDK 14+ 且字段可控 → Record 最省心;还在 Java 8/11 → 拷贝构造 + IDE generate 最安全;频率高+字段多 → MapStruct 最优。Cloneable 留给"不得不兼容老接口"的场景。
# 8.4 思考题
- Java 的
Object.clone()为什么默认是protected?Cloneable作为没有任何方法的标记接口,是好的设计还是设计失误?(提示:Joshua Bloch 在《Effective Java》第 13 条中详细解释了) @Scope("prototype")与本篇的原型模式是同一个东西吗?区别在哪?为什么这是 Spring 面试高频陷阱题?- 序列化深拷贝在高频场景(QPS > 100)下有多慢?如何量化?什么时候必须放弃偷懒、手写
clone()? - 如果
Resume的Education里又嵌套了Certification,这个深拷贝要递归到第几层?有什么通用的终止策略?
上一篇 03.建造者 → 本篇 → 05.静态代理:创建型 4 篇收束完毕,进入结构型模式——当一个对象被使用时,我们想"在不动它"的前提下加点料,代理是第一站。