设计模式:揭秘Java原型模式——让复杂对象的创建不再复杂

原型模式

原型模式介绍

定义: 原型模式(Prototype Design Pattern)用一个已经创建的实例作为原型,通过复制该原型对象来创建一个和原型对象相同的新对象。

西游记中的孙悟空,拔毛变小猴,孙悟空这种根据自己的形状复制出多个身外化身的技巧,在面向对象软件设计领域被称为原型模式。孙悟空就是原型对象。

在这里插入图片描述
原型模式主要解决的问题

  • 如果创建对象的成本比较大,比如对象中的数据是经过复杂计算才能得到,或者需要从RPC接口或者数据库等比较慢的IO中获取,这种情况我们就可以使用原型模式,从其他已有的对象中进行拷贝,而不是每次都创建新对象,进行一些耗时的操作。

原型模式原理

原型模式包含如下几种角色。

  • 抽象原型类(Prototype):它是声明克隆方法的接口,是所有具体原型类的公共父类,它可以是抽象类也可以是接口;
  • 具体原型类(ConcretePrototype):实现在抽象原型类中声明的克隆方法,在克隆方法中返回自己的一个克隆对象;
  • 客户类(Client):在客户类中,让一个原型对象克隆自身从而创建一个新的对象。由于客户类针对抽象原型类Prototype编程,因此用户可以根据需要选择具体原型类。系统具有较好的扩展性,增加或者替换具体原型类都比较方便。

在这里插入图片描述

深克隆与浅克隆

根据在复制原型对象的同时是否复制包含在原型对象中引用类型的成员变量 这个条件,原型模式的克隆机制分为两种,即浅克隆(Shallow Clone)和深克隆(Deep Clone)。

1) 什么是浅克隆

被复制对象的所有变量都含有与原来的对象相同的值,而所有的对其他对象的引用仍然指向原来的对象(克隆对象与原型对象共享引用数据类型变量)。
在这里插入图片描述

2) 什么是深克隆

除去那些引用其他对象的变量,被复制对象的所有变量都含有与原来的对象相同的值。那些引用其他对象的变量将指向被复制过的新对象,而不再是原有的那些被引用的对象。换言之,深复制把要复制的对象所引用的对象都复制了一遍。
在这里插入图片描述

Java中的Object类中提供了 clone() 方法来实现浅克隆。需要注意的是要想实现克隆的 Java 类必须实现一个标识接口 Cloneable ,来表示这个Java类支持被复制。

Cloneable接口是上面的类图中的抽象原型类,而实现了Cloneable接口的子实现类就是具体的原型类。代码如下:

3) 浅克隆代码实现:

public class ConcretePrototype implements Cloneable {public ConcretePrototype() {System.out.println("具体的原型对象创建完成!");}@Overrideprotected ConcretePrototype clone() throws CloneNotSupportedException {System.out.println("具体的原型对象复制成功!");return (ConcretePrototype)super.clone();}
}

测试

    @Testpublic void test01() throws CloneNotSupportedException {ConcretePrototype c1 = new ConcretePrototype();ConcretePrototype c2 = c1.clone();System.out.println("对象c1和c2是同一个对象?" + (c1 == c2));}

4) 深克隆代码实现

在ConcretePrototype类中添加一个对象属性为Person类型

public class ConcretePrototype implements Cloneable {private Person person;public Person getPerson() {return person;}public void setPerson(Person person) {this.person = person;}void show(){System.out.println("嫌疑人姓名: " +person.getName());}public ConcretePrototype() {System.out.println("具体的原型对象创建完成!");}@Overrideprotected ConcretePrototype clone() throws CloneNotSupportedException {System.out.println("具体的原型对象复制成功!");return (ConcretePrototype)super.clone();}}public class Person {private String name;public Person() {}public Person(String name) {this.name = name;}public String getName() {return name;}public void setName(String name) {this.name = name;}
}

测试

    @Testpublic void test02() throws CloneNotSupportedException {ConcretePrototype c1 = new ConcretePrototype();Person p1 = new Person();c1.setPerson(p1);//复制c1ConcretePrototype c2 = c1.clone();//获取复制对象c2中的Person对象Person p2 = c2.getPerson();p2.setName("峰哥");//判断p1与p2是否是同一对象System.out.println("p1和p2是同一个对象?" + (p1 == p2));c1.show();c2.show();}

打印结果

在这里插入图片描述

说明: p1与p2是同一对象,这是浅克隆的效果,也就是对具体原型类中的引用数据类型的属性进行引用的复制。

如果有需求场景中不允许共享同一对象,那么就需要使用深拷贝,如果想要进行深拷贝需要使用到对象序列化流 (对象序列化之后,再进行反序列化获取到的是不同对象)。 代码如下:

    @Testpublic void test03() throws Exception {ConcretePrototype c1 = new ConcretePrototype();Person p1 = new Person("峰哥");c1.setPerson(p1);//创建对象序列化输出流ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("c.txt"));//将c1对象写到文件中oos.writeObject(c1);oos.close();//创建对象序列化输入流ObjectInputStream ois = new ObjectInputStream(new FileInputStream("c.txt"));//读取对象ConcretePrototype c2 = (ConcretePrototype) ois.readObject();Person p2 = c2.getPerson();p2.setName("凡哥");//判断p1与p2是否是同一个对象System.out.println("p1和p2是同一个对象?" + (p1 == p2));c1.show();c2.show();}

打印结果:

在这里插入图片描述

注意:ConcretePrototype类和Person类必须实现Serializable接口,否则会抛NotSerializableException异常。

其实现在不推荐大家用Cloneable接口,实现比较麻烦,现在借助Apache Commons或者springframework可以直接实现:

  • 浅克隆:BeanUtils.cloneBean(Object obj);BeanUtils.copyProperties(S,T);
  • 深克隆:SerializationUtils.clone(T object);

BeanUtils是利用反射原理获得所有类可见的属性和方法,然后复制到target类。
SerializationUtils.clone()就是使用我们的前面讲的序列化实现深克隆,当然你要把要克隆的类实现Serialization接口。

4.5.4 原型模式应用实例

模拟某银行电子账单系统的广告信发送功能,广告信的发送都是有一个模板的,从数据库查出客户的信息,然后放到模板中生成一份完整的邮件,然后交给发送机进行发送处理。
在这里插入图片描述

发送广告信邮件UML类图

在这里插入图片描述

代码实现

  • 广告模板代码
/*** 广告信模板代码**/
public class AdvTemplate {//广告信名称private String advSubject = "xx银行本月还款达标,可抽iPhone 13等好礼!";//广告信内容private String advContext = "达标用户请在2022年3月1日到2022年3月30参与抽奖......";public String getAdvSubject() {return this.advSubject;}public String getAdvContext() {return this.advContext;}
}
  • 邮件类代码
package com.mashibing.example01;/*** 邮件类**/
public class Mail {//收件人private String receiver;//邮件名称private String subject;//称谓private String appellation;//邮件内容private String context;//邮件尾部, 一般是"xxx版权所有"等信息private String tail;//构造函数public Mail(AdvTemplate advTemplate) {this.context = advTemplate.getAdvContext();this.subject = advTemplate.getAdvSubject();}public String getReceiver() {return receiver;}public void setReceiver(String receiver) {this.receiver = receiver;}public String getSubject() {return subject;}public void setSubject(String subject) {this.subject = subject;}public String getAppellation() {return appellation;}public void setAppellation(String appellation) {this.appellation = appellation;}public String getContext() {return context;}public void setContext(String context) {this.context = context;}public String getTail() {return tail;}public void setTail(String tail) {this.tail = tail;}
}
  • 客户类
/*** 业务场景类**/
public class Client {//发送信息的是数量,这个值可以从数据库获取private static int MAX_COUNT = 6;//发送邮件public static void sendMail(Mail mail){System.out.println("标题: " + mail.getSubject() + "\t收件人: " + mail.getReceiver()+ "\t..发送成功!");}public static void main(String[] args) {//模拟邮件发送int i = 0;//把模板定义出来,数据是从数据库获取的Mail mail = new Mail(new AdvTemplate());mail.setTail("xxx银行版权所有");while(i < MAX_COUNT){//下面是每封邮件不同的地方mail.setAppellation(" 先生 (女士)");Random random = new Random();int num = random.nextInt(9999999);mail.setReceiver(num+"@"+"liuliuqiu.com");//发送 邮件sendMail(mail);i++;}}
}
  • 运行结果
    在这里插入图片描述

上面的代码存在的问题:

  • 发送邮件需要重复创建Mail类对象,而且Mail类的不同对象之间差别非常小,这样重复的创建操作十分的浪费资源;
  • 这种情况我们就可以使用原型模式,从其他已有的对象中进行拷贝,而不是每次都创建新对象,进行一些耗时的操作。

代码重构

  • Mail类
/*** 邮件类 实现Cloneable接口,表示该类的实例可以被复制* @author spikeCong* @date 2022/9/20**/
public class Mail implements Cloneable{//收件人private String receiver;//邮件名称private String subject;//称谓private String appellation;//邮件内容private String context;//邮件尾部, 一般是"xxx版权所有"等信息private String tail;//构造函数public Mail(AdvTemplate advTemplate) {this.context = advTemplate.getAdvContext();this.subject = advTemplate.getAdvSubject();}public String getReceiver() {return receiver;}public void setReceiver(String receiver) {this.receiver = receiver;}public String getSubject() {return subject;}public void setSubject(String subject) {this.subject = subject;}public String getAppellation() {return appellation;}public void setAppellation(String appellation) {this.appellation = appellation;}public String getContext() {return context;}public void setContext(String context) {this.context = context;}public String getTail() {return tail;}public void setTail(String tail) {this.tail = tail;}@Overridepublic Mail clone(){Mail mail = null;try {mail = (Mail)super.clone();} catch (CloneNotSupportedException e) {e.printStackTrace();}return mail;}
}
  • Client类
/*** 业务场景类* @author spikeCong* @date 2022/9/20**/
public class Client {//发送信息的是数量,这个值可以从数据库获取private static int MAX_COUNT = 6;//发送邮件public static void sendMail(Mail mail){System.out.println("标题: " + mail.getSubject() + "\t收件人: " + mail.getReceiver()+ "\t..发送成功!");}public static void main(String[] args) {//模拟邮件发送int i = 0;//把模板定义出来,数据是从数据库获取的Mail mail = new Mail(new AdvTemplate());mail.setTail("xxx银行版权所有");while(i < MAX_COUNT){//下面是每封邮件不同的地方Mail cloneMail = mail.clone();cloneMail.setAppellation(" 先生 (女士)");Random random = new Random();int num = random.nextInt(9999999);cloneMail.setReceiver(num+"@"+"liuliuqiu.com");//发送 邮件sendMail(cloneMail);i++;}}
}

4.5.5 原型模式总结

原型模式的优点

  1. 当创建新的对象实例较为复杂时,使用原型模式可以简化对象的创建过程,通过复制一个已有实例可以提高新实例的创建效率。

    比如,在 AI 系统中,我们经常需要频繁使用大量不同分类的数据模型文件,在对这一类文件建立对象模型时,不仅会长时间占用 IO 读写资源,还会消耗大量 CPU 运算资源,如果频繁创建模型对象,就会很容易造成服务器 CPU 被打满而导致系统宕机。通过原型模式我们可以很容易地解决这个问题,当我们完成对象的第一次初始化后,新创建的对象便使用对象拷贝(在内存中进行二进制流的拷贝),虽然拷贝也会消耗一定资源,但是相比初始化的外部读写和运算来说,内存拷贝消耗会小很多,而且速度快很多。

  2. 原型模式提供了简化的创建结构,工厂方法模式常常需要有一个与产品类等级结构相同的工厂等级结构(具体工厂对应具体产品),而原型模式就不需要这样,原型模式的产品复制是通过封装在原型类中的克隆方法实现的,无须专门的工厂类来创建产品。

  3. 可以使用深克隆的方式保存对象状态,使用原型模式将对象复制一份并将其状态保存起来,以便在需要的时候使用,比如恢复到某一历史状态,可以辅助实现撤销操作。

    在某些需要保存历史状态的场景中,比如,聊天消息、上线发布流程、需要撤销操作的程序等,原型模式能快速地复制现有对象的状态并留存副本,方便快速地回滚到上一次保存或最初的状态,避免因网络延迟、误操作等原因而造成数据的不可恢复。

原型模式缺点

  • 需要为每一个类配备一个克隆方法,而且该克隆方法位于一个类的内部,当对已有的类进行改造时需要修改源代码,违背了开闭原则。

使用场景

原型模式常见的使用场景有以下六种。

  • 资源优化场景。也就是当进行对象初始化需要使用很多外部资源时,比如,IO 资源、数据文件、CPU、网络和内存等。

  • 复杂的依赖场景。 比如,F 对象的创建依赖 A,A 又依赖 B,B 又依赖 C……于是创建过程是一连串对象的 get 和 set。

  • 性能和安全要求的场景。 比如,同一个用户在一个会话周期里,可能会反复登录平台或使用某些受限的功能,每一次访问请求都会访问授权服务器进行授权,但如果每次都通过 new 产生一个对象会非常烦琐,这时则可以使用原型模式。

  • 同一个对象可能被多个修改者使用的场景。 比如,一个商品对象需要提供给物流、会员、订单等多个服务访问,而且各个调用者可能都需要修改其值时,就可以考虑使用原型模式。

  • 需要保存原始对象状态的场景。 比如,记录历史操作的场景中,就可以通过原型模式快速保存记录。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/web/86413.shtml
繁体地址,请注明出处:http://hk.pswp.cn/web/86413.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Go语言-文件操作

基本介绍 文件是数据源&#xff0c;数据库也是一种特殊的文件。 Go语言中os.File结构体封装了文件的相关操作。 打开和关闭文件 -----打开文件----- file, err : os.Open("D:/111.txt") if err ! nil{fmt.Println("err ", err) }此时file就是一个指针&…

【电力物联网】云–边协同介绍

(꒪ꇴ꒪ )&#xff0c;Hello&#xff0c;我是祐言QAQ我的博客主页&#xff1a;C/C语言&#xff0c;数据结构&#xff0c;Linux基础&#xff0c;ARM开发板&#xff0c;网络编程等领域UP&#x1f30d;快上&#x1f698;&#xff0c;一起学习&#xff0c;让我们成为一个强大的技术…

《深入解析 C#(第 4 版)》推荐

《深入解析 C#&#xff08;第 4 版&#xff09;》推荐 在 C# 语言不断演进的技术浪潮中&#xff0c;《深入解析 C#&#xff08;第 4 版&#xff09;》犹如一座灯塔&#xff0c;为开发者照亮探索的道路。无论是经验丰富的老程序员&#xff0c;还是初入 C# 领域的新手&#xff0c…

【网络】Linux 内核优化实战 - net.core.netdev_max_backlog

目录 Linux 内核参数 net.core.netdev_max_backlog 详解一、参数概述二、参数功能与作用2.1 核心功能2.2 网络数据包处理流程 三、查看当前参数值3.1 通过 sysctl 命令3.2 直接读取 /proc/sys 文件 四、修改参数值4.1 临时修改&#xff08;立即生效&#xff0c;重启后失效&…

Nuitka 打包Python程序

文章目录 Nuitka 打包Python程序&#x1f680; **一、Nuitka 核心优势**⚙️ **二、环境准备&#xff08;Windows 示例&#xff09;**&#x1f4e6; **三、基础打包命令****单文件脚本打包****带第三方库的项目** &#x1f6e0;️ **四、高级配置选项****示例&#xff1a;完整命…

自动获取文件的内存大小怎么设置?批量获取文件名和内存大小到Excel中的方法

在对重要数据进行备份或迁移操作前&#xff0c;为确保备份全面无遗漏&#xff0c;且合理规划目标存储设备的空间&#xff0c;会将文件名和内存提取到 Excel。比如&#xff0c;某个部门要将旧电脑中的文件迁移到新服务器&#xff0c;提前整理文件信息&#xff0c;能清晰知道所需…

创建型设计模式——单例模式

单例设计模式 什么是创建型设计模式有哪些创建型设计模式 单例设计模式实现方法饿汉式单例懒汉式单例实现方法 CSDN——C单例模式详解 单例设计模式是一种创建型设计模式 什么是创建型设计模式 创建型设计模式&#xff0c;就是通过控制对象的创建方式来解决设计问题。 有哪…

html 照片环 - 图片的动态3D环绕

html 照片环 - 图片的动态3D环绕 引言一、源码二、图转base64参考链接 引言 效果展示&#xff1a; 一、源码 原始图片的base64编码字符太多了&#xff0c;博客放不下&#xff0c;将图片缩小后的加入html的源码如下&#xff1a; <!DOCTYPE html> <html><hea…

ADIOS2 介绍与使用指南

文章目录 ADIOS2 介绍与使用指南什么是ADIOS2?ADIOS2 的主要特点ADIOS2 核心概念ADIOS2 安装Linux 系统安装Windows 安装 ADIOS2 基本使用C 示例Python 示例 ADIOS2 高级特性并行I/O流模式 ADIOS2 引擎类型性能优化建议总结 ADIOS2 介绍与使用指南 什么是ADIOS2? ADIOS2(Ad…

网络安全 vs 信息安全的本质解析:数据盾牌与网络防线的辩证关系关系

在数字化生存的今天&#xff0c;每一次手机支付、每一份云端文档、每一条医疗记录的背后&#xff0c;都矗立着这两座安全堡垒。理解它们的协同逻辑&#xff0c;不仅是技术从业者的必修课&#xff0c;更是企业构建数字防护体系的底层认知 —— 毕竟当勒索软件同时切断 "护城…

ping-pong操作

常见不匹配的原因 瞬时数据率的差异&#xff1b; 数据顺序的差异&#xff1b; 对比维度PipelineFIFOPing-Pong逻辑复制结构类型时序分级推进&#xff08;寄存器链&#xff09;环形队列&#xff08;缓冲区&#xff09;双缓冲区&#xff08;轮换使用&#xff09;功能块并行&am…

21.合并两个有序链表

将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。 思路&#xff1a;这里使用的主要数据结构是单链表。该算法采用经典的双指针技术来合并列表。 A dummy node is created; this node does not hold any meaningful value b…

vue3中简单易懂说明nextTick的使用

nextTick(): 等待下一次 DOM 更新刷新的工具方法 重点解释: 当你在 Vue 中更改响应式状态时&#xff0c;最终的 DOM 更新并不是同步生效的&#xff0c;而是由 Vue 将它们缓存在一个队列中&#xff0c;直到下一个“tick”才一起执行。这样是为了确保每个组件无论发生多少状态改变…

gRPC 相关介绍

介绍 依赖两大技术 HTTP/2 作为传输协议 gRPC 底层用 HTTP/2&#xff0c;它支持&#xff1a; 多路复用&#xff08;在一条 TCP 连接中并行传输多个请求和响应&#xff09;二进制传输&#xff08;更紧凑、高效&#xff09;流式传输&#xff08;客户端流、服务端流、双向流&…

PyTorch 模型镜像下载与安装指南

在国内&#xff0c;由于网络限制&#xff0c;直接从 PyTorch 官方源下载可能会遇到速度慢或无法访问的问题。为了解决这一问题&#xff0c;可以使用国内镜像源来加速下载和安装 PyTorch。 文章目录 安装指定版本的 PyTorch&#xff08;以 CUDA 11.8 为例&#xff09;安装 CPU 版…

2025年SVN学习价值分析

⚖️ 一、SVN的现状与应用场景分析 仍在特定领域发挥作用 传统企业维护场景&#xff1a;在金融、电信、政府等采用集中式开发流程的机构中&#xff0c;许多遗留系统仍使用SVN管理。这些系统往往体量庞大、架构稳定&#xff0c;迁移成本高&#xff0c;因此SVN短期内不会被完全替…

JavaScript中的10种排序算法:从入门到精通

作为前端开发者&#xff0c;排序算法是我们必须掌握的基础知识。无论是在面试中&#xff0c;还是在实际开发中处理数据展示时&#xff0c;排序都是一个常见需求。今天&#xff0c;我将用通俗易懂的方式&#xff0c;带你了解JavaScript中最常见的10种排序算法。 1. 冒泡排序 - …

【微信小程序】6、SpringBoot整合WxJava获取用户手机号

1、手机号快速验证组件 手机号快速验证组件 旨在帮助开发者向用户发起手机号申请&#xff0c;并且必须经过用户同意后&#xff0c;开发者才可获得由平台验证后的手机号&#xff0c;进而为用户提供相应服务。 该能力与手机号实时验证组件的区别为&#xff1a; 手机号快速验证…

redis8.0新特性:原生JSON支持详解

文章目录 一、写在前面二、使用1、基本命令&#xff08;1&#xff09;JSON.SET 设置 JSON 值&#xff08;2&#xff09;JSON.GET 获取 JSON 值&#xff08;3&#xff09;JSON.DEL 删除 JSON 值&#xff08;4&#xff09;JSON.MGET 批量获取&#xff08;5&#xff09;JSON.MSET …

QT网络调试助手开发全指南,软件设计图预研,后续文档跟进补充

网络调试助手 1 TCP网络调试助手 1.1 项目概述 网络相关的一些基础概念学习QTcpServer 学习QTcpClient 学习TextEdit特定位置输入文字颜色学习网络通信相关知识点 复习巩固之前UI控件 程序运行如下图所示 1.2 开发流程 1.3 QTtcp 服务器的关键流程 工程建立&#xff0c;需要在…