Java 23 种设计模式:从踩坑到精通 | 原型模式 —— 克隆对象,深拷贝与浅拷贝的坑你踩过吗?
Java 23 种设计模式:从踩坑到精通 | 原型模式 —— 克隆对象,深拷贝与浅拷贝的坑你踩过吗?
摘要:当对象的创建成本高昂(如需要复杂的数据库查询或大量计算),或者需要保留对象某一时刻的快照时,直接
new可能既低效又繁琐。原型模式通过“克隆”已有对象来创建新实例,将复制逻辑封装在原型类中。本文从复制粘贴的需求出发,深入剖析 Java 中的Cloneable接口、浅拷贝与深拷贝的本质区别,以及序列化、Spring Bean 作用域中克隆的应用,帮你彻底掌握拷贝的“深浅”之道。
📖《Java 23 种设计模式:从踩坑到精通》
开篇:系列介绍与目录 | 上一篇:建造者模式 |当前:原型模式| 下一篇:适配器模式
🔗 返回系列总目录
1. 从一个“复制”需求说起
假设你正在开发一个报告系统,需要生成大量格式相同、但具体数据略有差异的报表对象。每个报表对象创建时都需要读取模板、查询数据库、计算样式等,成本很高。如果每次都从头new,性能会极差。
我们自然会想:“能不能直接复制一个已有对象,再修改差异部分?” 这种需求在图形编辑器(复制图形)、游戏开发(复制敌人)中也非常常见。然而,直接用=赋值只是复制了引用,修改副本会污染原对象;如果简单粗暴地new一个再手动赋值,又可能遗漏某些深层字段。
原型模式(Prototype Pattern)正是为了解决这类问题:通过拷贝原型实例来创建新对象,而无需重新执行高昂的初始化过程。
1.1 你的场景该不该用原型?
| 判断标准 | 是 → 用原型 | 否 → 用 new / 工厂 |
|---|---|---|
| 对象创建成本极高(如 IO、网络、复杂计算) | ✅ | ❌ |
| 需要保留对象某一时刻的快照(撤销/重做) | ✅ | ❌ |
| 对象间差异很小,仅少量属性需要修改 | ✅ | ❌ |
| 对象构造简单,且无重复创建需求 | ❌ | ✅ |
2. 模式定义与 UML 结构
原型模式用原型实例指定创建对象的种类,并通过拷贝这些原型创建新的对象。它属于创建型设计模式。
标准原型模式的 UML 类图:
两个核心角色:
- 抽象原型(Prototype):声明克隆自身的接口,通常是
Cloneable或自定义接口; - 具体原型(ConcretePrototype):实现克隆方法,返回自身的副本。
💡核心理解:原型模式让对象自己负责“如何复制自己”,外界只需调用
clone(),无需了解内部结构。
3. Java 中的克隆基础:浅拷贝 vs 深拷贝
Java 提供了Object.clone()方法和Cloneable接口来实现原型模式,但默认的clone()是浅拷贝。
- 浅拷贝:复制对象时,只复制基本类型的值和引用类型的引用地址。原对象和副本共享同一个引用对象,修改副本的引用字段会影响原对象。
- 深拷贝:不仅复制对象本身,还递归复制所有引用的对象,生成完全独立的副本。
3.1 浅拷贝示例(踩坑现场)
publicclassReportimplementsCloneable{privateStringtitle;privateList<String>data;// 引用类型publicReport(Stringtitle,List<String>data){this.title=title;this.data=data;}@OverridepublicReportclone(){try{return(Report)super.clone();// 默认浅拷贝}catch(CloneNotSupportedExceptione){thrownewRuntimeException(e);}}// getters/setters...}测试:
List<String>data=newArrayList<>();data.add("原始数据");Reportoriginal=newReport("日报",data);Reportcopy=original.clone();copy.getData().add("新增数据");System.out.println(original.getData());// [原始数据, 新增数据] → 原对象被污染!⚠️踩坑了:浅拷贝导致原对象和副本共用一个
data列表,修改副本会影响原对象。
3.2 深拷贝解决方案
方案一:手动递归复制
@OverridepublicReportclone(){try{Reportcloned=(Report)super.clone();cloned.data=newArrayList<>(this.data);// 复制集合returncloned;}catch(CloneNotSupportedExceptione){thrownewRuntimeException(e);}}✅ 简单直接,但需要为每个引用字段手动编写复制代码。
方案二:序列化深拷贝(适用于对象层次深的场景)
publicReportdeepClone(){try{ByteArrayOutputStreambos=newByteArrayOutputStream();ObjectOutputStreamoos=newObjectOutputStream(bos);oos.writeObject(this);ByteArrayInputStreambis=newByteArrayInputStream(bos.toByteArray());ObjectInputStreamois=newObjectInputStream(bis);return(Report)ois.readObject();}catch(IOException|ClassNotFoundExceptione){thrownewRuntimeException(e);}}✅ 通用性强,但要求
Report及其所有引用类实现Serializable接口,且序列化开销较大。
4. 代码实现:图形编辑器中的原型(含原型管理器)
4.1 抽象原型
publicinterfaceShapeextendsCloneable{Shapeclone();voiddraw();}4.2 具体原型:矩形
publicclassRectangleimplementsShape{privateintwidth;privateintheight;privateColorcolor;// 假设 Color 是自定义的复杂对象,需实现 CloneablepublicRectangle(intwidth,intheight,Colorcolor){this.width=width;this.height=height;this.color=color;}@OverridepublicRectangleclone(){try{Rectanglecloned=(Rectangle)super.clone();cloned.color=this.color.clone();// 深拷贝 Color 对象returncloned;}catch(CloneNotSupportedExceptione){thrownewRuntimeException(e);}}@Overridepublicvoiddraw(){System.out.println("绘制矩形[宽:"+width+",高:"+height+",颜色:"+color+"]");}}4.3 原型管理器(缓存常用原型)
publicclassShapeCache{privatestaticMap<String,Shape>cache=newHashMap<>();publicstaticvoidloadCache(){Rectanglerect=newRectangle(100,50,newColor("红色"));cache.put("red_rect",rect);}publicstaticShapegetShape(Stringid){returncache.get(id).clone();// 返回克隆副本,不污染原型}}4.4 客户端
ShapeCache.loadCache();Shapeshape1=ShapeCache.getShape("red_rect");Shapeshape2=ShapeCache.getShape("red_rect");shape1.draw();shape2.draw();// shape1 和 shape2 是独立对象,修改互不影响✅ 通过原型管理器,客户端只需从缓存取原型并克隆,完全屏蔽了构造细节。
5. 原型模式 vs 工厂模式 vs 建造者模式
| 对比维度 | 原型模式 | 工厂模式 | 建造者模式 |
|---|---|---|---|
| 核心作用 | 通过拷贝已有实例创建新对象 | 封装对象创建的类型选择 | 分步骤构建复杂对象 |
| 创建方式 | clone()内存复制 | new出具体产品 | 一步步设置参数后build() |
| 适用场景 | 创建成本高、需要保留对象状态 | 产品种类多,需要统一管理创建逻辑 | 参数多且可选,构建过程复杂 |
| 性能考量 | 避免重新初始化,高效 | 普通 new,无特殊优势 | 与工厂类似 |
💡原型模式的性能优势只有在构造过程非常昂贵时才凸显,简单对象直接
new更直接。
6. 优缺点一览
| 优点 | 缺点 |
|---|---|
| 性能高:直接内存复制,避免耗时的初始化过程 | 深拷贝实现复杂:每个关联对象都要支持克隆,有循环引用时更麻烦 |
| 简化创建过程:客户端无需知道具体类名 | 必须实现Cloneable接口,有一定侵入性 |
| 动态增加产品:运行时通过注册新原型扩展系统 | clone()方法使用不当可能引入浅拷贝 Bug |
| 可保存状态:创建对象快照,支持撤销操作 |
7. 框架与实践中的应用
7.1 JDK:Object.clone() 与 Cloneable
所有 Java 类都继承clone(),但需实现Cloneable并重写为public才能正常使用。
7.2 Spring Bean 作用域:Prototype
当 Bean 作用域设为prototype时,每次getBean()都会创建一个新实例。若结合原型管理器,可将模板对象缓存,克隆出多个实例。
7.3 MyBatis 对象映射
MyBatis 查询后将数据库记录映射为 Java 对象,底层大量使用 Bean 拷贝(如 CGLIB 或反射),体现了原型思想。
8. 常见误区与面试高频题
❌ 误区1:clone()默认就是深拷贝Object.clone()是浅拷贝,必须手动实现深拷贝。
❌ 误区2:原型模式一定比 new 快
只有在构造过程非常昂贵(如涉及 IO、网络、大量计算)时才有优势,简单对象反而因为克隆机制有额外开销。
💡 面试高频追问
- 深拷贝和浅拷贝的区别?→ 浅拷贝共享引用,深拷贝完全独立。
- 如何实现深拷贝?→ 手动递归复制、序列化/反序列化、或使用
Cloneable层层克隆。 - Spring 的
prototype作用域和原型模式的关系?→ 思想上类似,都是创建多实例;但 Spring 是通过getBean()动态创建,不一定是克隆。 - 原型模式如何破坏单例?→ 如果单例类实现
Cloneable并重写clone(),可能通过克隆生成第二个实例。
🎉恭喜:如果你能立刻说出浅拷贝和深拷贝的区别,并知道如何通过序列化实现深拷贝,你已经超越了 80% 的初级 Java 开发者。面试中问到“如何安全地复制一个复杂对象”,你的答案就是原型模式。
9. 六大设计原则在原型模式中的体现
| 设计原则 | 体现 |
|---|---|
| 单一职责(SRP) | 原型类自己负责拷贝逻辑,职责内聚 |
| 开闭原则(OCP) | 可以通过注册新原型扩展系统,无需修改客户端代码 |
| 里氏替换(LSP) | 客户端依赖抽象原型,具体原型克隆后仍可替换 |
| 依赖倒置(DIP) | 客户端依赖抽象Shape,不关心具体类 |
| 接口隔离(ISP) | 原型接口仅包含clone(),精简 |
| 迪米特法则(LoD) | 客户端只与原型管理器交互,不知内部结构 |
🧭 《Java 23 种设计模式:从踩坑到精通》快速导航
- 开篇:系列介绍与目录
- 上一篇:建造者模式 —— 构造器参数太多?试试链式调用
- 当前:原型模式 —— 克隆对象,深拷贝与浅拷贝的坑你踩过吗?(你在这里)
- 下一篇:适配器模式 —— 让不兼容的接口也能一起工作 🚧 即将发布
- 创建型模式汇总:单例、工厂、建造者、原型
- 结构型模式汇总:适配器、装饰器、代理……
- 行为型模式汇总:观察者、策略、模板方法……
🔔 关注《Java 23 种设计模式:从踩坑到精通》,用 25 篇文章彻底吃透设计模式。
📦福利预告:全系列代码及 UML 源码将在完结时统一打包开放,点击「关注」「收藏」第一时间获取。
🚀下一篇:适配器模式 —— 让不兼容的接口也能一起工作!🚧 即将发布,敬请关注!
📌 除了设计模式,我也在深挖智能物流实战(WMS、托盘调度、机器学习落地)。欢迎点击头像,看看专栏 《出版社物流WMS智能调度实战》。技术相通,思路可鉴。
