编程进阶网 编程进阶网
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • 面向对象设计

  • 常见设计原则

  • 巧学设计模式

    • README
    • 单例模式设计思想
    • 工厂模式设计思想
    • 建造者模式设计思想
    • 原型模式设计思想
      • 01.案例引入与思考
        • 1.1 痛点场景
        • 1.2 它哪里不舒服
        • 1.3 引出本篇主角
      • 02.直觉方案探索
        • 2.1 尝试:手动逐字段 copy(拷贝构造函数)
        • 2.2 尝试:反射拷贝(BeanUtils.copyProperties)
        • 2.3 尝试:序列化深拷贝(最偷懒方案)
        • 2.4 三次失败之后——需求清单收敛
      • 03.原型模式基础
        • 3.1 从失败中提炼的骨架
        • 3.2 原型模式定义
        • 3.3 典型使用场景
        • 3.4 三个角色的标准结构
      • 04.深浅拷贝对比
        • 4.1 核心要点
        • 4.2 🧪 邮件案例:浅拷贝踩坑
        • 4.3 🧪 深拷贝修复版
        • 4.4 🧪 回到简历事故的完整改造
        • 4.5 进阶:原型管理器
      • 05.用前用后效果对比
      • 05.用前用后效果对比
        • 5.1 安全性与创建耗时对比
        • 5.2 批量创建性能对比
        • 5.3 不同拷贝方式性能量化
        • 5.4 回到开篇的简历事故
        • 5.5 核心收益
      • 06.反面踩坑实录
        • 6.1 忘了实现 Cloneable,运行期崩溃
        • 6.2 浅拷贝多线程"埋雷"
        • 6.3 clone 绕过构造函数——不变量失效
        • 6.4 序列化深拷贝吃光 CPU
        • 6.5 替代方案汇总
      • 07.决策树与选型
        • 7.1 该不该用原型模式
        • 7.2 选型清单速查
        • 7.3 和工厂模式的选型分界
      • 08.总结与延伸
        • 8.1 演化逻辑沉淀
        • 8.2 模式联动边界
        • 8.3 真实开源代码中的原型
        • 8.4 现代替代:Cloneable 之外的选择
        • 8.4 思考题
    • 静态代理设计模式
    • 动态代理设计模式
    • 适配器模式设计思想
    • 装饰者模式设计思想
    • 外观模式设计思想
    • 桥接模式设计思想
    • 组合模式设计思想
    • 享元模式设计思想
    • 观察者模式设计思想
    • 策略者模式设计思想
    • 模版模式设计思想
    • 迭代器模式设计思想
    • 职责链模式设计思想
    • 命令模式设计思想
    • 状态模式设计思想
    • 备忘录模式设计思想
    • 中介者模式设计思想
    • 访问者模式设计思想
    • 解释器模式设计思想
    • 23种设计模式概括
    • 技术写作模板
  • 系统架构设计

  • 编程
  • 巧学设计模式
杨充
2025-11-03
目录

原型模式设计思想

# 第三卷第4章:原型模式设计思想

📚 本篇渐进学习节奏(建议按顺序食用)

  1. 第 01 节 · 案例引入 — 一段"简历模板共享引用"导致用户数据串号的线上事故
  2. 第 02 节 · 直觉探索 — 拷贝构造/反射/序列化——三条直觉方案全翻了车
  3. 第 03 节 · 模式基础 — 从失败诉求中提炼标准骨架
  4. 第 04 节 · 深浅拷贝对比 — 浅拷贝踩坑 → 深拷贝修复,邮件案例精炼版
  5. 第 05 节 · 效果对比 — 事故改造前后 + 批量性能数据
  6. 第 06 节 · 反面踩坑 — Cloneable 四种经典翻车+替代方案
  7. 第 07 节 · 决策选型 — 决策树 + 选型清单
  8. 第 08 节 · 总结延伸 — 沉淀 + 模式联动 + 开源案例

阅读到任何一节卡壳,直接跳回上一节复盘场景;本篇代码均可直接运行。

# 目录介绍

  • 01.案例引入与思考
    • 1.1 痛点场景
    • 1.2 它哪里不舒服
    • 1.3 引出本篇主角
  • 02.直觉方案探索
    • 2.1 尝试拷贝构造函数
    • 2.2 尝试反射拷贝
    • 2.3 尝试序列化深拷贝
    • 2.4 需求清单收敛
  • 03.原型模式基础
    • 3.1 标准骨架
    • 3.2 原型模式定义
    • 3.3 典型使用场景
  • 04.深浅拷贝对比
    • 4.1 核心要点
    • 4.2 浅拷贝踩坑
    • 4.3 深拷贝修复
  • 05.用前用后效果对比
    • 5.1 安全性与创建耗时
    • 5.2 批量创建性能
    • 5.3 核心收益
  • 06.反面踩坑实录
    • 6.1 忘了实现Cloneable
    • 6.2 浅拷贝多线程埋雷
    • 6.3 clone绕过构造函数
    • 6.4 序列化吃光CPU
    • 6.5 替代方案汇总
  • 07.决策树与选型
    • 7.1 该不该用原型模式
    • 7.2 选型清单速查
  • 08.总结与延伸
    • 8.1 演化逻辑沉淀
    • 8.2 模式联动边界
    • 8.3 开源实例
    • 8.4 思考题

# 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;
    // 还有十几个字段...
}
1
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());
// ...
1
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
1
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("李四");
1
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
1
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
}
1
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,没人全记得补
1
2
3
4
5
6

❌ 失败原因:拷贝构造函数是"靠人肉维护"的方案——每加一个字段,所有复制点都要补。30 个字段 × 10 处复制 = 300 个潜在遗漏点。

💡 反思:我们需要一种"字段自动发现"的机制——加字段后拷贝逻辑自动生效,不需要人肉同步。

# 2.2 尝试:反射拷贝(BeanUtils.copyProperties)

【新人方案②:用 Spring 的 BeanUtils 自动拷贝】

// 方案 B:反射自动拷贝,不用手动写逐字段复制
Resume a = new Resume();
BeanUtils.copyProperties(template, a);
// 一行搞定!新增字段自动生效!
1
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
1
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();
}
1
2
3
4
5
6
7
8

🧪 跑一下,看会怎样

// 单次耗时:5~20ms(取决于对象图大小)
// QPS 1000 的场景:CPU 直接打满,GC 频繁触发
// 而且:所有字段必须实现 Serializable——第三方类搞不定
1
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 类不该抛此异常
        }
    }
}
1
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();                        // 不关心深浅,拿到就能用
1
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"——原件被污染!
1
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"——原件不变!
1
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();
1
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("张三");
1
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("张三");
1
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("清华"));  // ✅ 只改自己,模板不受影响
1
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
1
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 各自独立,内部共享引用
1
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
1
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();
1
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
1
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
    原型 -.替代.-> 工厂[工厂模式]
    原型 -.造原型.-> 建造者[建造者模式]
    原型 -.配合.-> 单例[单例模式]
    原型 -.返回快照.-> 备忘录[备忘录]
    原型 -.复制结构.-> 组合[组合模式]
1
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 处拷贝点 = 维护噩梦
}
1
2
3
4
5

② MapStruct / Lombok——编译期生成,零手写

@Mapper
public interface ResumeMapper {
    ResumeMapper INSTANCE = Mappers.getMapper(ResumeMapper.class);
    Resume copy(Resume source);  // 编译期生成逐字段拷贝,比反射快 50×
}
1
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));
    }
}
1
2
3
4
5

选择建议:如果项目已经 JDK 14+ 且字段可控 → Record 最省心;还在 Java 8/11 → 拷贝构造 + IDE generate 最安全;频率高+字段多 → MapStruct 最优。Cloneable 留给"不得不兼容老接口"的场景。

# 8.4 思考题

  1. Java 的 Object.clone() 为什么默认是 protected?Cloneable 作为没有任何方法的标记接口,是好的设计还是设计失误?(提示:Joshua Bloch 在《Effective Java》第 13 条中详细解释了)
  2. @Scope("prototype") 与本篇的原型模式是同一个东西吗?区别在哪?为什么这是 Spring 面试高频陷阱题?
  3. 序列化深拷贝在高频场景(QPS > 100)下有多慢?如何量化?什么时候必须放弃偷懒、手写 clone()?
  4. 如果 Resume 的 Education 里又嵌套了 Certification,这个深拷贝要递归到第几层?有什么通用的终止策略?

上一篇 03.建造者 → 本篇 → 05.静态代理:创建型 4 篇收束完毕,进入结构型模式——当一个对象被使用时,我们想"在不动它"的前提下加点料,代理是第一站。

上次更新: 2026/06/17, 11:43:57
建造者模式设计思想
静态代理设计模式

← 建造者模式设计思想 静态代理设计模式→

最近更新
01
信号崩溃快速排查
06-15
02
CoreDump破案
06-15
03
perf火焰图实战
06-15
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 桂ICP备2024034950号 | 桂公网安备45142202000030
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式