☰
Java原型模式:浅克隆、深克隆与clone避坑实战
2026/10/29 16:14:55 网站建设 项目流程

简介:这份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.mainClass=com.example.prototype.Client # 跑邮件复制演示 mvn -q exec:java -Dexec.mainClass=com.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; } /** * 抽象原型声明的克隆契约。 * 访问修饰符必须是 public:Object.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是值类型的String,attachment是引用类型。一条链路上两种字段类型全齐了。

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 里成立的理论基础,值得逐条抠。

第一条,对任何对象 x,x.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 对象持有 B,B 又持有 A,递归克隆直接栈溢出。还有集合类型,List<Attachment>里的每个元素都得遍历克隆一遍,Map更难受,键和值都要处理。手写深克隆在结构简单的时候还行,对象图一复杂就是给自己挖坑。

序列化方案绕开了这些问题。ObjectOutputStream内部维护一个引用句柄表,遇到已经写过的对象直接写句柄,不会重复展开,循环引用天然安全。所有嵌套层级、集合容器、数组,全部交给对象流处理,你只需要保证沿途每个类都实现了Serializable。代价是性能——序列化要走反射拿字段,还要处理类型描述符,单次开销通常是手写克隆的三到五倍。

还有一条折中路线是用 JSON 序列化做深拷贝,转成字符串再转回对象。写起来短,但会丢类型信息(泛型擦除后拿不到元素类型)、丢transient字段、日期格式容易出岔子。我在业务代码里只在结构极简的 DTO 上用过,领域对象一律不用。

4.3 浅克隆与深克隆的选型对照

两种克隆方式没有优劣,只有合不合适。判断的时候按这张表过一遍:

维度浅克隆深克隆
实现方式implements Cloneable+super.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是0,createTime是null。原型对象上这些字段明明都有值。

原因。序列化会跳过transient和static字段,这两类字段不写进字节流。反序列化重建对象时也不会调用构造函数,字段值完全靠流里恢复,流里没有的就保持默认值。createTime如果在构造函数里赋值,反序列化后也是null。

解决。真正需要跨序列化保留的字段就别标transient,transient只留给确实不该被序列化的东西(比如数据库连接、文件句柄)。如果字段天生不可序列化又必须保留,就在类里实现writeObject和readObject手工补;或者干脆在deepClone()返回前手动回填派生字段。构造函数里的初始化逻辑,反序列化时一律不执行,这一点得记牢。

5.4 现象:克隆对象塞进 HashSet,contains 返回 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 CloneStrategy<T> { 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 往往不在克隆那一行,而在三个星期后某个调用方顺手改了一下共享对象。这套模板加上策略枚举,就是我给这类需求留的后悔药。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询