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

【从0开始学设计模式-6| 原型模式】

一个月没更新了,在找实习。。

其实还是懒了,其实每天花个半小时左右就能写一篇博客的。。。

概念

原型模式(Prototype Pattern)设计出来的目标就是:通过本体复制出与本体一样的分身(分身具有本体一样特性)

定义:使用原型实例指定要创建的对象的种类,并通过复制此原型来创建新对象。

首先,应该注意原型模式不是用来获得性能优势的。它仅用于从原型实例创建新对象。

原型模式的结构

在原型模式结构图中包含如下几个角色:

  • Prototype(抽象原型类):声明克隆方法的接口,是所有具体原型类的公共父类,可以是抽象类也可以是接口,甚至还可以是具体实现类。
  • ConcretePrototype(具体原型类):它实现在抽象原型类中声明的克隆方法,在克隆方法中返回自己的一个克隆对象。
  • Client(客户类):让一个原型对象克隆自身从而创建一个新的对象,在客户类中只需要直接实例化或通过工厂方法等方式创建一个原型对象,再通过调用该对象的克隆方法即可得到多个相同的对象。由于客户类针对抽象原型类Prototype编程,因此用户可以根据需要选择具体原型类,系统具有较好的可扩展性,增加或更换具体原型类都很方便。

原型式的实现

关键就是:浅克隆(浅复制)和深克隆(深复制)之间的区别

类图设计

Level是一个关卡,我们的目的就是克隆一个关卡!

实现1:浅拷贝

Java的浅拷贝使用的是Object类自带的clone()方法,注意使用这个方法类必须加上implements Cloneable,这相当于一个标记,否则无法使用Object.clone()方法。调用时会直接抛出CloneNotSupportedException

  • 关卡类以及关卡中的相关类

    @DatapublicclassLevelimplementsCloneable{privateStringname;privateStringdescription;privateMonster[]monsters;//怪兽数组privateRewardsrewards;@OverridepublicLevelclone(){//重写的Object的clone方法try{//实现浅克隆return(Level)super.clone();}catch(CloneNotSupportedExceptione){System.err.println("对象不支持克隆");returnnull;}}}
    @Data@AllArgsConstructorpublicclassMonster{//怪兽的相关属性privateStringname;privateinthp;privateintattack;}
    @Data@AllArgsConstructorpublicclassRewards{//奖励privateintgold;}
  • 客户端

    publicclassClient{publicstaticvoidmain(String[]args){//创建原型对象Levelproto=newLevel();proto.setName("猴王出世");proto.setDescription("让猴王成功出世,可以通关本关卡");Monster[]monsters={newMonster("小猴子",100,1),newMonster("大狮子",200,100),};proto.setMonsters(monsters);proto.setRewards(newRewards(50));//克隆Levelclone=proto.clone();//比较克隆结果System.out.println(clone==proto);//falseSystem.out.println(clone.getRewards()==proto.getRewards());//trueSystem.out.println(clone.getMonsters()[0]==proto.getMonsters()[0]);//trueclone.getRewards().setGold(60);System.out.println("原来的对象"+" "+proto.getRewards().getGold());//60System.out.println("克隆的对象"+" "+clone.getRewards().getGold());//60}}
解释
  • 浅拷贝的特点
    • 对于基本数据类型以及String,拷贝的是值。
      • String是不可变的引用对象,所以也是拷贝的值。
    • 而对于引用数据类型,只拷贝了“内存地址”。也就是说克隆的对象跟原来的对象共用一套引用对象
      • 例如案例中的monsters数组和rewards对象。当克隆对象修改了引用对象的值,原对象的引用对象的值也会跟着变。上面案例中的客户端有测试这个效果。
    • 注意:拷贝出来的对象本身,是新创建的。super.clone()会在堆内存中申请一块全新的空间。只不过里面的引用类型的成员变量monsters数组和rewards对象不是新的。
  • 造成这种现象的原因如下:
    • Object类的clone()方法默认是浅拷贝:除了基本数据类型跟String类型的变量,各自修改之后是独有的。对于其他引用类型的变量都是共享的,因为克隆的只是引用。也要注意String虽然是引用对象,但是修改之后也是独有的,是因为String被设计成了不可变
  • 如何实现深拷贝呢?
    • 见下文。

实现2:深拷贝(序列化、反序列化)

这是一种实现深拷贝的方式:利用序列化、反序列化

注意:这种写法要求Level以及里面的引用对象所属的类必须必须implements Serializable,这样才能成功进行序列化、反序列化

  • 关卡类以及关卡中的相关类

    @DatapublicclassLevelimplementsSerializable{privateStringname;privateStringdescription;privateMonster[]monsters;privateRewardsrewards;publicLeveldeepClone(){try{//将对象写入到输出流中ByteArrayOutputStreamos=newByteArrayOutputStream();ObjectOutputStreamoos=newObjectOutputStream(os);//序列化:递归地遍历Level对象,把它包含的String、Monster[]数组、Rewards对象全部转换成一串连续的二进制字节流oos.writeObject(this);//将对象从输入流中取出ByteArrayInputStreamis=newByteArrayInputStream(os.toByteArray());ObjectInputStreamois=newObjectInputStream(is);//反序列化:读取这串二进制流,并在内存中重新构建出一套一模一样的对象Levelclone=(Level)ois.readObject();//关闭流os.close();oos.close();is.close();ois.close();returnclone;}catch(Exceptione){System.err.println("克隆失败");returnnull;}}}
    @Data@AllArgsConstructor@NoArgsConstructorpublicclassMonsterimplementsSerializable{privateStringname;privateinthp;privateintattack;}
    @Data@AllArgsConstructor@NoArgsConstructorpublicclassRewardsimplementsSerializable{privateintgold;}
  • 客户端

    publicclassClient{publicstaticvoidmain(String[]args){//创建原型对象Levelproto=newLevel();proto.setName("猴王出世");proto.setDescription("让猴王成功出世,可以通关本关卡");Monster[]monsters={newMonster("小猴子",100,1),newMonster("大狮子",200,100),};proto.setMonsters(monsters);proto.setRewards(newRewards(50));//克隆Levelclone=proto.deepClone();//比较克隆结果System.out.println(clone==proto);//falseSystem.out.println(clone.getRewards()==proto.getRewards());//falseSystem.out.println(clone.getMonsters()[0]==proto.getMonsters()[0]);//false}}
解释重点代码

其实关键就是使用了序列化跟反序列化

//将对象写入到输出流中ByteArrayOutputStreamos=newByteArrayOutputStream();ObjectOutputStreamoos=newObjectOutputStream(os);oos.writeObject(this);//将对象从输入流中取出ByteArrayInputStreamis=newByteArrayInputStream(os.toByteArray());ObjectInputStreamois=newObjectInputStream(is);Levelclone=(Level)ois.readObject();

具体流程是这样的:

  • new ByteArrayOutputStream();内存中开辟一块空地,专门用来存放二进制数据(字节)

  • ObjectOutputStream是一个转换器,用来将Java对象->二进制的0、1(序列化)

  • new ObjectOutputStream(os);是将转换器连接到内存中开辟的空地,而不是真正的在写。

  • writeObject(this)是真正将对象写到输出流的操作,通过oos对象转换成二进制数据然后放到开辟的空地

  • os.toByteArray()空地的数据变成真正的字节数组

  • new ByteArrayInputStream(os.toByteArray());建立了一个读取通道

  • ObjectInputStream也是一个转换器,将二进制的0、1 -> Java对象(反序列化)

  • new ObjectInputStream(is);转换器连接到通道

  • (Level) ois.readObject();真正的读取操作,在堆内存开辟新空间创建新对象,读取通道里面的数据,再通过转换器还原数据,最后放到新对象中

通过这种方式,构建出来的Monster[]数组和Rewards对象,虽然内容和原来一样,但在内存里拥有了全新的地址

其他实现方式

这两种实现方式,是我在背小林coding的八股的时候发现的

实现 Cloneable 接口并重写 clone() 方法

要求对象及其所有引用类型字段都实现Cloneable 接口,并且重写clone() 方法。通过递归克隆引用类型字段来实现深拷贝。

classMyClassimplementsCloneable{privateStringfield1;privateNestedClassnestedObject;@OverrideprotectedObjectclone()throwsCloneNotSupportedException{// 首先调用 super.clone() 得到一个浅拷贝的对象MyClasscloned=(MyClass)super.clone();// 对内部的引用对象进行深拷贝cloned.nestedObject=(NestedClass)nestedObject.clone();// 深拷贝内部的引用对象returncloned;}}classNestedClassimplementsCloneable{privateintnestedField;@OverrideprotectedObjectclone()throwsCloneNotSupportedException{returnsuper.clone();}}
手动递归复制

针对特定对象结构,手动递归复制对象及其引用类型字段

classMyClass{privateStringfield1;privateNestedClassnestedObject;publicMyClassdeepCopy(){MyClasscopy=newMyClass();copy.setField1(this.field1);copy.setNestedObject(this.nestedObject.deepCopy());//手动对引用类型的字段进行拷贝returncopy;}}classNestedClass{privateintnestedField;publicNestedClassdeepCopy(){NestedClasscopy=newNestedClass();copy.setNestedField(this.nestedField);returncopy;}}

原型模式适用环境

主要优点

  • 当创建新的对象实例较为复杂时,使用原型模式可以简化对象的创建过程,通过复制一个已有实例可以提高新实例的创建效率。
  • 扩展性较好,由于在原型模式中提供了抽象原型类,在客户端可以针对抽象原型类进行编程,将具体原型实现类写在配置文件中,增加或减少产品类对原有系统都没有任何影响。
  • 原型模式提供了简化的创建结构,工厂方法模式常常需要有一个与产品类等级结构相同的工厂等级结构,而原型模式就不需要这样,原型模式中产品的复制是通过封装在原型类中的克隆方法实现的,无须专门的工厂类来创建产品。
  • 可以使用深克隆的方式保存对象的状态,使用原型模式将对象复制一份并将其状态保存起来,以便在需要的时候使用(如恢复到某一历史状态),可辅助实现撤销操作。

主要缺点

  • 需要为每一个类配备一个克隆方法,而且该克隆方法位于一个类的内部,当对已有的类进行改造时,需要修改源代码,违背了“开闭原则”
  • 在实现深克隆时需要编写较为复杂的代码,而且当对象之间存在多重的嵌套引用时,为了实现深克隆,每一层对象对应的类都必须支持深克隆,实现起来可能会比较麻烦。

适用环境

在以下情况下可以考虑使用原型模式:

  • 创建新对象成本较大(如初始化需要占用较长的时间,占用太多的CPU资源或网络资源),新的对象可以通过原型模式对已有对象进行复制来获得,如果是相似对象,则可以对其成员变量稍作修改。
  • 如果系统要保存对象的状态,而对象的状态变化很小,或者对象本身占用内存较少时,可以使用原型模式配合备忘录模式来实现。
  • 需要避免使用分层次的工厂类来创建分层次的对象,并且类的实例对象只有一个或很少的几个组合状态,通过复制原型对象得到新实例可能比使用构造函数创建一个新实例更加方便。

说一下例子,进一步理解原型模式的使用场景

有时候,一个对象的可重用性不在于它的类定义,而在于它当前的状态

  • 示例:在游戏开发中,你创建了一个“精英怪”。这个怪物的血量、护甲、装备和 AI 状态都是在游戏运行中动态计算出来的。
  • 痛点:如果你想要再生成一个一模一样的精英怪,通过new一个新对象再挨个属性赋值(setHealth,setArmor…)太繁琐且易错。
  • 原型方案:直接enemy.clone(),瞬间得到一个状态完全一致的副本。

如果这篇文章对你有帮助,欢迎点赞、评论、关注、收藏。你们的支持是我前进的动力!

http://www.jsqmd.com/news/595604/

相关文章:

  • Swagger Client 完整教程:从零开始构建强大的 API 集成应用
  • 文件上传漏洞的花式绕过:用Pikachu靶场复现企业级攻防场景
  • Sony FCB-EV9500L LVDS图像闪烁问题分析
  • STM32F469NI+LVGL双缓冲与DMA2D硬件加速实战
  • 网站SEO关键词对网页排名的重要性如何评估
  • Kandinsky-5.0-I2V-Lite-5s应用场景:游戏NPC立绘动态化+过场动画快速生成
  • 手机生成剧本杀软件2025推荐,创新剧情设计工具助力创作
  • SDMatte算法原理浅析:从卷积神经网络看图像分割技术
  • 5分钟部署Fun-ASR语音识别:支持中文、英文、日文等31种语言
  • Java企业级集成:Qwen3-ASR-0.6B语音质检系统开发
  • 融合LoRA微调模型:打造专属领域的AI修图专家系统
  • 自动驾驶中的ICP:激光SLAM定位模块是如何用点云匹配实现厘米级精度的?
  • SEO_为什么你的SEO策略无效?常见原因与解决办法(372 )
  • 伏羲天气预报可信AI:预报结果置信度输出、不确定性传播与可视化
  • 从read()到硬盘:用strace和bpftrace动态追踪Linux内核文件读取的完整路径(附实战脚本)
  • 编写程序实现智能乐器音准检测偏差时,提示“需要调音”,新手也能调好音。
  • 5分钟搞定AI绘画:Asian Beauty Z-Image Turbo快速部署与使用教程
  • 7个Linux系统管理员面试常见技术盲点及解决方案终极指南 [特殊字符]
  • CoPaw复杂逻辑推理与数学解题能力极限测试
  • AI绘画作品集:Anything V5图像生成服务实际效果与案例分享
  • 告别信道束缚:探究 Random Multiplexing 随机复用技术
  • Leather Dress Collection 实战:为开源项目自动生成 README 与贡献指南
  • 港大新作GS-SDF开源了!手把手教你用激光雷达+3DGS复现IROS2025论文效果(附避坑指南)
  • Qwen2.5-VL-32B-Instruct 实战:从零搭建视觉语言模型微调环境(附常见错误解决)
  • 交互弹窗设计避坑指南:Toast、Dialog、Actionbar和Snackbar的常见错误与优化建议
  • KuiklyUI布局系统完全指南:Flexbox与绝对定位实战
  • NaViL-9B开发者调试手册:nvidia-smi显存监控+ss端口诊断全流程
  • CLIP-GmP-ViT-L-14入门指南:理解ImageNet/ObjectNet双基准评估意义
  • Kandinsky-5.0-I2V-Lite-5s多风格测试:卡通、写实、水墨画生成效果对比
  • 阿里达摩院神器实测:RexUniNLU开箱即用,智能客服理解力飙升