当前位置: 首页 > news >正文

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智能调度实战》。技术相通,思路可鉴。

http://www.cnnetsun.cn/news/2804769.html

相关文章:

  • 30天无限循环:JetBrains IDE试用期重置终极指南
  • 点云标注避坑指南:用CloudCompare保存带语义标签的PLY文件,为什么选ASCII格式?
  • 别再死记硬背了!用Anki记忆库+Notion模板,科学攻克国科大英语Unit1核心句型与行文结构
  • 别再只会用默认Key了!手把手教你用ysoserial探测并利用Shiro 1.2.4反序列化漏洞
  • 交直流混联系统优化|基于显式拓扑变量可靠性评估的双Q交直流混合配电网优化规划研究(Python代码实现)
  • 从智能灯泡到传感器网络:实战解析蓝牙Mesh、WiFi AP/STA、ZigBee 3.0在智能家居中的真实配置与避坑
  • STM32F411/F401 Keil裸机工程模板:带LED闪烁、串口基础驱动和一键清理功能
  • SQL中CASE WHEN的实战心法:从数据分层到业务规则固化
  • XUnity.AutoTranslator:5分钟搞定Unity游戏多语言翻译的终极指南
  • Win/Mac双平台实测:手把手解决Operator Mono字体在VSCode中不生效的常见问题
  • 告别乱码!手把手教你用LabVIEW 2023报表工具包完美读取带中文的Excel表格
  • 深入DPDK L3fwd源码:看一个三层转发示例如何管理路由与端口
  • 百度网盘高速下载终极方案:告别限速的智能解析工具
  • 三分钟快速上手:Dell G15开源散热控制神器tcc-g15完整指南
  • 效率提升秘籍:用快马生成ubuntu自动化部署脚本,十分钟搞定服务器环境配置
  • 从‘压控’原理到电路设计:搞懂MOS管G、S、D,让你的开关电源效率翻倍
  • VC++ MFC二维码识别工具:调用ZBar实现摄像头/图片扫码功能
  • 别再只会conda clean了!遇到InvalidArchiveError,试试这个更治本的修复思路
  • 【非IT人AI营销实战指南】:3步开通CSDN AI数字营销,零代码搞定获客闭环?
  • Vite 构建性能调优:如何通过分包与插件优化将打包耗时缩短 70%
  • Julia数据工程实战:高性能ETL管道设计与优化
  • 【分享】手机散热器 游戏党降温神器
  • 100皇后GA实战:编码约束、纯变异设计与可行性优先架构
  • Gemma 2 2B轻量级大模型性能重定义与实测指南
  • 视觉SLAM‘抗干扰’指南:从光流法到概率模型,5种动态物体剔除方案全解析
  • RK3568双网口配置实战:RMII模式下的gmac0与gmac1 DTS设置详解与对比
  • Windows点云处理DLL:集成PCL1.8.1+VTK8.1,支持读写/滤波/重建/拾取
  • Web Speech API语音识别靠谱吗?实测Chrome、Edge、Firefox的兼容性与避坑指南
  • 保姆级教程:用PyTorch手写CBAM注意力模块(附完整代码与避坑指南)
  • Git目录泄露后快速重建本地仓库的纯命令行恢复工具,开箱即用无需安装依赖