
简介这份PPT面向软件设计模式初学者、Java开发者及课程汇报者聚焦创建型模式中的原型模式帮助理清对象复制、浅拷贝与深拷贝、clone()方法及Cloneable接口等核心知识。内容以概述、使用场景与优缺点、浅克隆和深克隆、代码示例为主线结合抽象原型、具体原型与客户类角色演示clone()实现及异常处理并讨论创建成本、状态一致性等设计取舍。压缩包仅含1个pptx文件大小约5.93MB页面组织集中便于课堂展示、专题分享或课后自学翻阅。已有192人学习适合用于设计模式课程复习、Java原型模式实现参考以及汇报材料整理。1. 从一条被打满的压测曲线说起原型模式到底省掉了什么设计模式里的原型模式说白了就一句话拿一个已经造好的对象复制一份再改。我接手过一个报表导出模块一晚上要造几万条结构相同的工单对象构造函数里挂着权限查询和字典翻译CPU 直接被打满后来把初始化好的那条记录 clone 一份再改字段压测曲线立刻平了下来。这份 PPT 就是围绕原型模式 Prototype Pattern 做的一次完整梳理Java 语言四段结构概述、使用场景与优缺点、浅克隆和深克隆、代码示例代码示例用邮件复制这个场景贯穿附件类加邮件类的组合刚好能把引用共享的问题暴露出来。它适合两类人一类是要在设计模式课上讲清楚「为什么复制比 new 划算」的人另一类是手上真有一批昂贵对象要批量派生的开发。全篇没有花哨的 UML 工具输出重点压在 clone 的语义和两种克隆方式的差别上这部分恰恰是面试和实战里最容易翻车的地方。2. 讲稿骨架拆解四段目录如何从概念推到可运行代码2.1 四段权重与推进顺序为什么先讲 clone 契约再讲场景把四段目录摊开看篇幅分配很不均匀。概述段塞了三件事原型模式的定义与意图、Java 里 clone() 方法的背景、以及 x.clone() ! x 这类契约条款顺带把三个角色一次性摆出来。使用场景段讲的是什么时候该用、用了要付什么代价。浅克隆与深克隆段是全篇的技术核心把「成员变量是值类型还是引用类型」这条分水岭讲透。代码示例段用邮件复制把前面三段全部验证一遍。这个顺序是有讲究的。很多讲设计模式的材料喜欢一上来甩类图听众盯着三个方框两条箭头脑子里没有任何具体对象五分钟后就只剩手机屏幕亮着。这份材料反过来先用「创建过程复杂、需要频繁创建」这个痛点建立动机再把 Java 语言层面的机制摆出来——因为原型模式在 Java 里落地绕不开 Object.clone()绕不开 Cloneable 这个空标记接口这些不是可选知识是硬门槛。真正讲的时候概述段控制在五分钟以内最合适把意图和契约说清楚就往下走。场景段要展开讲因为它决定了听众以后什么时候会想起来用这个模式。深浅克隆段建议现场跑代码改一个字段看两个对象是不是一起变比讲十遍定义都有用。代码示例段放在最后作为前面所有结论的验收。还有一点值得注意这份材料默认读者有基本的 Java 面向对象基础和 UML 读图能力抽象类、接口、继承这些概念没有铺垫。如果是给零基础的人讲概述段前面得自己补一段对象创建的背景否则「用原型实例指定创建对象的种类」这句话会直接卡住一半人。2.2 三个角色的职责边界Prototype、ConcretePrototype、Client原型模式的结构非常扁平三个角色一层继承。但每个角色的边界划在哪里直接决定了代码写出来是干净还是拧巴。角色职责示例中的对应类必须做的事抽象原型类 Prototype声明克隆接口收敛所有具体原型的公共契约Prototype抽象类实现Cloneable声明clone()具体原型类 ConcretePrototype被复制的对象本体实现自身的克隆逻辑ConcretePrototypeA、ConcretePrototypeB、Email重写clone()返回自身类型客户类 Client持有原型引用发起克隆请求Client、邮件示例里的调用方不关心构造细节只调clone()抽象原型类在这份材料里用的是abstract class而不是interface这不是随手写的。Cloneable是个空标记接口本身不带来任何方法真正干活的clone()定义在java.lang.Object上访问修饰符是protected。如果抽象原型写成接口并声明clone()接口方法的默认可见性是public实现类就得把Object的protected clone()提升成public才能满足接口还要处理受检异常声明的一致性凭空多出一堆约束。用抽象类继承Object的clone()再把可见性往上提一级路子顺得多。具体原型类的clone()返回值写成了自己的类型比如ConcretePrototypeA clone()这叫协变返回类型。好处很直接调用方拿到的就是具体类型不用再强转。如果返回值统一写成Prototype客户类每次克隆完都得补一句(ConcretePrototypeA)代码里全是括号。客户类的职责被压到了最轻。它不知道对象是怎么 new 出来的不知道初始化里挂了多少远程调用只知道手里这个原型能 clone。这种解耦在需要动态切换实现的时候特别值钱——运行时把一个ConcretePrototypeB换进去客户代码一行不用改。2.3 从类图落到包结构一份可以直接开工的目录骨架PPT 里的类图是静态的落到工程里得有个能跑起来的骨架。我一般会按角色层和业务示例层分开建包讲的时候可以一层一层往里翻也方便单独跑某一层的 main 方法。prototype-demo/ ├── pom.xml └── src/main/java/com/example/prototype/ ├── Prototype.java # 抽象原型类 ├── ConcretePrototypeA.java # 具体原型 A ├── ConcretePrototypeB.java # 具体原型 B ├── Client.java # 角色演示入口 └── mail/ ├── Attachment.java # 附件类引用类型成员 ├── Email.java # 邮件类含浅克隆与深克隆两版 └── MailClient.java # 邮件复制演示入口分层理由很实在。前四个类纯粹演示角色关系代码不到五十行适合单独跑一遍看类型和哈希值mail包放业务场景附件和邮件凑成一对组合关系专门用来演示引用字段在浅克隆下的共享行为。两个入口分开讲课时切终端标签页就行不用改代码。pom.xml里没什么依赖指定 Java 8 以上就行序列化方案在哪个版本都能跑。编译和运行直接用 Maven# 编译 mvn -q clean compile # 跑角色演示 mvn -q exec:java -Dexec.mainClasscom.example.prototype.Client # 跑邮件复制演示 mvn -q exec:java -Dexec.mainClasscom.example.prototype.mail.MailClient如果你不想引入exec-maven-plugin直接javac加java -cp target/classes也一样。跑起来之后先别急着看输出对不对把System.out.println里的对象地址和字段值挨个对一遍确认「克隆对象和原对象不是同一个实例但类型一致」这条契约真的成立了再往下看浅克隆的问题。3. 浅克隆落地Cloneable 加 super.clone() 的最小闭环3.1 抽象原型类 Prototype 怎么写可见性、协变返回与异常声明抽象原型类是整个结构的地基写歪了后面全是补丁。这份材料里的写法很典型但有几个点容易被抄漏。public abstract class Prototype implements Cloneable { private String id; public String getId() { return id; } public void setId(String id) { this.id id; } /** * 抽象原型声明的克隆契约。 * 访问修饰符必须是 publicObject.clone() 是 protected * 跨包调用时客户类看不到必须在这里提权。 */ Override public Prototype clone() throws CloneNotSupportedException { // super.clone() 是 Object 的 native 方法做的是逐字段浅拷贝 return (Prototype) super.clone(); } }第一处是implements Cloneable。这个接口没有任何方法纯标记JVM 在Object.clone()里检查它没有就直接抛CloneNotSupportedException。少写这一句编译期一声不吭运行时炸。第二处是可见性。Object.clone()是protected同包或者子类内部能调客户类跨包调不到。所以抽象原型里必须重写成public把这条通道打开。很多人抄代码时把Override public漏掉结果客户类编译不过回头一看以为自己类名写错了。第三处是异常声明。CloneNotSupportedException是受检异常super.clone()声明了它子类重写时不能擅自去掉除非内部先 catch 掉。这份材料选择一路往外抛让调用方决定怎么处理这在演示代码里没问题生产代码里我一般选择在clone()内部 catch 后转成IllegalStateException因为一个正确实现了Cloneable的类这个异常理论上永远不会发生把它暴露给业务代码纯属添乱。第四处是返回类型。抽象类里先返回Prototype子类重写时可以改成自己的具体类型这是 Java 5 之后支持的协变返回类型。链条传下去最末端的客户类拿到的就是具体类型。3.2 邮件复制场景的浅克隆链路Email 与 Attachment 的引用共享邮件复制这个例子的选材很好因为邮件天生是一堆字段加一个附件的组合结构sender、receiver、subject、body、date是值类型的Stringattachment是引用类型。一条链路上两种字段类型全齐了。public class Email implements Cloneable { private String sender; private String receiver; private String subject; private String body; private String date; private Attachment attachment; // 引用类型成员浅克隆的关键观察点 public Email(String sender, String receiver, String subject, String body, String date, Attachment attachment) { this.sender sender; this.receiver receiver; this.subject subject; this.body body; this.date date; this.attachment attachment; } public Attachment getAttachment() { return attachment; } public void setBody(String body) { this.body body; } Override public Email clone() throws CloneNotSupportedException { // 只调用 super.clone()attachment 字段复制的是引用地址 return (Email) super.clone(); } public String display() { return 发送者: sender 接收者: receiver 标题: subject 内容: body 日期: date 附件: attachment; } }Attachment类本身很简单一个download()方法加默认的Object实现没有重写clone()也不需要实现Cloneable——这一点很关键浅克隆根本不会去碰它。调用方这样写public class MailClient { public static void main(String[] args) throws Exception { Attachment attachment new Attachment(); // 示例里的收发件人用占位字符串实际业务里换成真实数据 Email original new Email(发件人A, 收件人B, 原型设计, 邮件复制, 2025-01-01, attachment); Email cloned original.clone(); cloned.setBody(浅克隆后的正文); // 改值类型字段互不影响 // 关键断言两个 Email 的 attachment 指向同一块内存 System.out.println(original.getAttachment() cloned.getAttachment()); // true System.out.println(original.display()); System.out.println(cloned.display()); } }跑出来的结果里cloned的body变成了新值original的body纹丝不动这符合预期但两个对象的attachment打印出来是同一个对象标识比较返回true。也就是说如果此刻cloned.getAttachment()上调一次会改变内部状态的方法original那边会同步看到效果。这不是 bug这就是浅克隆的定义——成员变量是值类型就复制一份是引用类型就只复制地址。很多人在这一步会误判成「克隆失败」。其实克隆成功了只是深度不够。判断标准很简单你希望克隆体和原型共享这个成员吗希望就是浅克隆够用不希望就得往下走深克隆。3.3 Object.clone 的三条契约与 CloneNotSupportedException 的触发条件PPT 概述段列的三条契约是整个原型模式在 Java 里成立的理论基础值得逐条抠。第一条对任何对象 xx.clone() ! x必须为真。克隆出来的是新实例内存地址不同。这条好理解但有个反例如果clone()被重写成直接return this契约就破了而且这种写法在某些「节省对象」的诡异优化里真的出现过后果是两个变量名指向同一个对象改一个另一个跟着变排查起来极其费劲。第二条x.clone().getClass() x.getClass()必须为真。克隆体和原型类型一致。Object.clone()的 native 实现保证了这一点它按原对象的内存布局复制不会改变类型。但如果你在clone()里手动 new 了一个别的类型返回这条就废了。第三条如果equals()定义恰当x.clone().equals(x)应该成立。注意「定义恰当」这个限定词。默认的Object.equals()比的是引用地址克隆体地址不同这条自然不成立。只有当你按业务字段重写了equals()这条才有意义。反过来说如果你的equals()依赖了被浅克隆共享的可变字段克隆后两边状态一改这条也会失效——这是第 5 章要展开的坑。CloneNotSupportedException的触发条件只有一条类没有实现Cloneable接口却调用了Object.clone()。注意这个检查发生在运行时编译器完全不管。所以「类写了clone()方法但忘了加implements Cloneable」这种错误能一路编译通过上线之后在某个分支上炸掉。我见过一次出问题的类三个月才被调用到日志里就一行异常连堆栈都没打全。顺带说一个容易忽略的细节数组的clone()是public的而且返回类型就是数组本身不需要强转。但数组的clone()同样是浅拷贝元素是引用类型时依然共享。这条在复制List转数组的结果时经常被忘掉。4. 深克隆落地用对象流把引用关系彻底断开4.1 序列化深克隆的完整实现writeObject 到 readObject深克隆要解决的问题是不管成员变量是值类型还是引用类型全部复制一份克隆体和原型之间不留任何共享引用。Java 里最省事的路子是把整个对象图序列化再反序列化——写出去的时候按内容写读回来的时候按内容重建天然就是一份完全独立的副本。前提条件是两个类都得实现Serializable。import java.io.*; public class Attachment implements Serializable { private static final long serialVersionUID 1L; private String fileName; public Attachment(String fileName) { this.fileName fileName; } public void download() { System.out.println(下载附件 fileName); } Override public String toString() { return Attachment{ fileName }; } }serialVersionUID显式声明不是可选项。不写的话 JVM 会按类结构自动生成一个只要类加了个字段或者改了个方法签名这个值就变之前序列化出去的数据全部反序列化失败。演示代码里不写顶多报个错重来生产环境里这意味着缓存里的对象全部失效。邮件类的深克隆改造public class Email implements Serializable { private static final long serialVersionUID 1L; private String sender; private String receiver; private String subject; private String body; private String date; private Attachment attachment; public Email(String sender, String receiver, String subject, String body, String date, Attachment attachment) { this.sender sender; this.receiver receiver; this.subject subject; this.body body; this.date date; this.attachment attachment; } public Attachment getAttachment() { return attachment; } /** * 深克隆借对象流把整个对象图写出去再读回来。 * 全程在内存里完成不落磁盘。 */ public Email deepClone() throws IOException, ClassNotFoundException { // 1. 序列化把 this 写进字节数组 ByteArrayOutputStream bos new ByteArrayOutputStream(); try (ObjectOutputStream oos new ObjectOutputStream(bos)) { oos.writeObject(this); } // 2. 反序列化从字节数组重建一个新对象 try (ByteArrayInputStream bis new ByteArrayInputStream(bos.toByteArray()); ObjectInputStream ois new ObjectInputStream(bis)) { return (Email) ois.readObject(); } } public String display() { return 发送者: sender 附件: attachment; } }几个参数和写法值得说明。ByteArrayOutputStream是内存缓冲toByteArray()把字节取出来喂给输入流整个过程不碰磁盘所以性能比文件序列化高得多。两个流都用 try-with-resources 包起来ObjectOutputStream不关会漏缓冲区里的尾部数据反序列化时直接抛EOFException。oos.writeObject(this)写的是整个对象图只要字段能追到的对象都要求可序列化追不到就抛NotSerializableException异常信息里会带类名定位很快。调用侧对比一下就很清楚Attachment attachment new Attachment(季度报表.pdf); Email original new Email(发件人A, 收件人B, 原型设计, 邮件复制, 2025-01-01, attachment); Email cloned original.deepClone(); System.out.println(original.getAttachment() cloned.getAttachment()); // false cloned.getAttachment().download(); // 只影响克隆体自己的附件返回false说明两个附件已经是两块独立内存深克隆到位了。4.2 为什么 super.clone() 撑不起深克隆很多人第一次听说深克隆第一反应是在clone()里对每个引用字段再调一次clone()写成这样Override public Email clone() throws CloneNotSupportedException { Email copy (Email) super.clone(); // 想让附件也独立再克隆一次 copy.attachment (Attachment) this.attachment.clone(); return copy; }这段代码有两个问题。一是Attachment必须也实现Cloneable并重写clone()否则一句CloneNotSupportedException糊脸上。二是如果附件里还嵌着别的引用字段那个字段得继续递归克隆一层套一层代码量和维护成本随对象图的深度线性增长。更麻烦的是循环引用。A 对象持有 BB 又持有 A递归克隆直接栈溢出。还有集合类型ListAttachment里的每个元素都得遍历克隆一遍Map更难受键和值都要处理。手写深克隆在结构简单的时候还行对象图一复杂就是给自己挖坑。序列化方案绕开了这些问题。ObjectOutputStream内部维护一个引用句柄表遇到已经写过的对象直接写句柄不会重复展开循环引用天然安全。所有嵌套层级、集合容器、数组全部交给对象流处理你只需要保证沿途每个类都实现了Serializable。代价是性能——序列化要走反射拿字段还要处理类型描述符单次开销通常是手写克隆的三到五倍。还有一条折中路线是用 JSON 序列化做深拷贝转成字符串再转回对象。写起来短但会丢类型信息泛型擦除后拿不到元素类型、丢transient字段、日期格式容易出岔子。我在业务代码里只在结构极简的 DTO 上用过领域对象一律不用。4.3 浅克隆与深克隆的选型对照两种克隆方式没有优劣只有合不合适。判断的时候按这张表过一遍维度浅克隆深克隆实现方式implements Cloneablesuper.clone()Serializable 对象流或手写递归引用字段行为与原型共享同一对象完全独立的新对象单次开销极低native 方法按内存布局复制较高序列化通常慢 3 到 5 倍前置条件无对象图内所有类实现Serializable循环引用不受影响对象流自动处理手写递归会栈溢出典型适用字段全是不可变值、或共享引用是期望行为克隆后要独立修改嵌套对象主要风险误改共享对象导致原型被污染transient字段丢失、对象图过大拖慢性能表格之外还有一条实践判断如果原型里的引用字段指向的是不可变对象String、Integer、LocalDate、Collections.unmodifiableList的结果浅克隆和深克隆在可见行为上完全等价这时候硬上深克隆纯属浪费。反过来如果引用字段是可变的集合或者业务实体又确实需要在克隆体上改那就老老实实做深克隆别指望调用方「记得不要改」。5. 避坑记录clone 用错一次能查半天5.1 现象改了克隆对象的附件原型对象的附件跟着变现象。客户代码从原型克隆出一封邮件改完正文准备换附件调了cloned.getAttachment().setFileName(新附件.pdf)结果回头打印原邮件附件名也变了。当时的日志里两封邮件看起来一模一样一度怀疑是打印方法写错了。原因。super.clone()是逐字段的位拷贝attachment这个字段复制的是引用地址两个Email实例的attachment指向堆上同一个Attachment对象。通过任何一条引用改它的内部状态另一条自然看得见。解决。按业务需要选一条要独立副本就换成序列化深克隆只改这一个字段就在clone()里补一句copy.attachment new Attachment(this.attachment.getFileName())彻底一点把Attachment设计成不可变对象所有字段final改附件名只能新建一个那就从源头上堵住了共享可变状态。我一般选第三条因为前两条都得靠调用方自觉。5.2 现象运行时报 CloneNotSupportedException现象。本地跑得好好的换到另一台机器上跑集成测试clone()那一行稳定抛异常堆栈只指到Object.clone看不出哪个类出的问题。原因。具体原型类重写了clone()并调用了super.clone()但类声明上漏了implements Cloneable。或者父类实现了Cloneable子类一路继承下来但中间某一层重写clone()时把接口去掉了。这个错误编译期完全不报Cloneable是标记接口不走它就没有任何编译约束。解决。给对应类补上implements Cloneable并且检查整条继承链上每一层都保留了这个标记。更稳的做法是写一个最小单元测试对着每个支持克隆的类调一遍clone()跑一次就知道谁漏了。这条测试成本极低收益极高。5.3 现象深克隆之后 transient 字段和派生字段没了现象。深克隆出来的对象密码字段是null缓存计算出来的totalAmount是0createTime是null。原型对象上这些字段明明都有值。原因。序列化会跳过transient和static字段这两类字段不写进字节流。反序列化重建对象时也不会调用构造函数字段值完全靠流里恢复流里没有的就保持默认值。createTime如果在构造函数里赋值反序列化后也是null。解决。真正需要跨序列化保留的字段就别标transienttransient只留给确实不该被序列化的东西比如数据库连接、文件句柄。如果字段天生不可序列化又必须保留就在类里实现writeObject和readObject手工补或者干脆在deepClone()返回前手动回填派生字段。构造函数里的初始化逻辑反序列化时一律不执行这一点得记牢。5.4 现象克隆对象塞进 HashSetcontains 返回 false现象。把克隆出来的对象放进HashSet再拿原型对象去contains返回false可这两个对象明明是「同一个东西」。原因。没有重写equals()和hashCode()走的是Object的引用比较两个实例地址不同自然不相等。另一种更隐蔽的情况是重写了equals()但依赖了被浅克隆共享的可变字段——克隆之后改了一方的字段两边的hashCode变了对象在哈希桶里的位置和它的哈希值对不上contains就查不着了。解决。按业务主键重写equals()和hashCode()并且保证参与计算的字段在对象放进哈希容器之后不再变动。这条建议对所有可变对象都成立不只是克隆出来的。如果确实需要可变对象做键就别用哈希容器或者用IdentityHashMap明确按引用比较。5.5 现象换成原型模式后批量创建反而更慢现象。本来是new一批对象听说原型模式性能好就换成了深克隆结果压测下来吞吐掉了一半。原因。深克隆走序列化每克隆一次要创建两个字节流、写一遍对象图、读一遍对象图单次开销是new的好几倍。原型模式的收益来自「省掉昂贵的初始化」如果对象的构造函数本来就只是个字段赋值那原型模式不但没收益还多了克隆的开销。解决。回到成本判断上只有当初始化涉及远程调用、复杂计算、大批量数据装配的时候复制才比新建划算。对象结构简单、构造轻量的时候直接new。另外如果原型本身是稳定不变的可以缓存一份克隆好的模板反复用而不是每次都从原对象深克隆——但要注意模板一旦被污染就全线崩所以模板必须是不可变对象。6. 二次开发把两份示例合成一套可切换的克隆策略6.1 用策略接口隔离浅克隆与深克隆PPT 里的邮件类有两版浅克隆版和深克隆版实际项目里不可能维护两个Email。我的做法是保留一个类把两种克隆方式抽成策略用枚举选择。public interface CloneStrategyT { T copy(T source) throws Exception; } public enum CloneMode { SHALLOW { Override public T extends Cloneable T copy(T source) throws Exception { // 走 Object.clone()要求 source 的类重写并提权了 clone() Method m source.getClass().getMethod(clone); return (T) m.invoke(source); } }, DEEP { Override public T extends Serializable T copy(T source) throws Exception { // 走对象流要求整条对象图可序列化 ByteArrayOutputStream bos new ByteArrayOutputStream(); try (ObjectOutputStream oos new ObjectOutputStream(bos)) { oos.writeObject(source); } try (ObjectInputStream ois new ObjectInputStream( new ByteArrayInputStream(bos.toByteArray()))) { return (T) ois.readObject(); } } }; public abstract T T copy(T source) throws Exception; }调用方写成CloneMode.DEEP.copy(original)切模式只改一个枚举值。这样改造的好处是把「用哪种克隆」从类设计问题降级成了配置问题后期要加第三种策略比如基于 JSON 的深拷贝只需新增一个枚举常量已有的业务类一行不动。6.2 用两条断言把语义钉死在测试里克隆语义最容易在重构中悄悄变化所以我习惯用两条 JUnit 断言把它钉死Test void shallowCloneSharesAttachment() throws Exception { Email original new Email(A, B, 标题, 正文, 2025-01-01, new Attachment(报表.pdf)); Email cloned (Email) original.clone(); assertNotSame(original, cloned); // 契约一不是同一实例 assertSame(original.getAttachment(), cloned.getAttachment()); // 浅克隆共享引用 } Test void deepCloneDetachesAttachment() throws Exception { Email original new Email(A, B, 标题, 正文, 2025-01-01, new Attachment(报表.pdf)); Email cloned original.deepClone(); assertNotSame(original, cloned); assertNotSame(original.getAttachment(), cloned.getAttachment()); // 深克隆引用断开 }assertSame和assertNotSame比打印日志靠谱得多CI 上跑一次就知道有没有人把deepClone偷偷改回super.clone()。从那以后我每写一个clone()方法都先把这两条断言写好再写实现跑绿了才往下接业务逻辑。血泪经验是克隆的 bug 往往不在克隆那一行而在三个星期后某个调用方顺手改了一下共享对象。这套模板加上策略枚举就是我给这类需求留的后悔药。希望帮到你。本文还有配套的精品资源点击获取