🌲本文收录于专栏《源码中的设计模式》——理论与实战的完美结合
作者其它优质专栏推荐:
📚《技术专家修炼》——搞技术,进大厂,聊人生三合一专栏
📚《leetcode 300题》——每天一道算法题,进大厂必备
📚《糊涂算法》——从今天起,迈过数据结构和算法这道坎
📚《从实战学python》——Python的爬虫,自动化,AI等实战应用
点击跳转到文末领取粉丝福利
哈喽,大家好,我是一条~
之前的《白话设计模式》因为工作被搁置,如今再次启航,并搭配框架源码解析一起食用,将理论与实战完美结合。
对设计模式不是很熟悉的同学可以先看一下《23种设计模式的一句话通俗解读》全面的了解一下设计模式,形成一个整体的框架,再逐个击破。
今天我们一块看一下原型模式,属于简单且常用的一种。
定义
官方定义
用原型实例指定创建对象的种类,并且通过拷贝这个原型来创建新的对象。
通俗解读
在需要创建重复的对象,为了保证性能,本体给外部提供一个克隆体进行使用。
类似我国的印刷术,省去new
的过程,通过copy
的方式创建对象。
结构图
代码实现
目录结构
建议跟着一条学设计模式的小伙伴都建一个
maven
工程,并安装lombok
依赖和插件。并建立如下包目录,便于归纳整理。
pom
如下
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.10</version>
</dependency>
开发场景
假设一条开发了一个替代Mybatis
的框架,叫YitiaoBatis
,每次操作数据库,从数据库里面查出很多记录,但是改变的部分是很少的,如果每次查数据库,查到以后把所有数据都封装一个对象,就会导致要new
很多重复的对象,造成资源的浪费。
一条想到一个解决办法,就是把查过的数据保存起来,下来查相同的数据,直接把保存好的对象返回,也就是缓存的思想。
我们用代码模拟一下:
1.创建Yitiao
实体类
/**
* author:一条
*/
@Data
@AllArgsConstructor
public class Yitiao {
private String name;
private Integer id;
private String wechat;
public Yitiao(){
System.out.println("Yitiao对象创建");
}
}
2.创建YitiaoBatis
类
/**
* author:一条
*/
public class YitiaoBatis {
//缓存Map
private Map<String,Yitiao> yitiaoCache = new HashMap<>();
//从缓存拿对象
public Yitiao getYitiao(String name){
//判断缓存中是否存在
if (yitiaoCache.containsKey(name)){
Yitiao yitiao = yitiaoCache.get(name);
System.out.println("从缓存查到数据:"+yitiao);
return yitiao;
}else {
//模拟从数据库查数据
Yitiao yitiao = new Yitiao();
yitiao.setName(name);
yitiao.setId(1);
yitiao.setWechat("公众号:一条coding");
System.out.println("从数据库查到数据:"+yitiao);
//放入缓存
yitiaoCache.put(name,yitiao);
return yitiao;
}
}
}
3.编写测试类
/**
* author:一条
*/
public class MainTest {
public static void main(String[] args) {
YitiaoBatis yitiaoBatis = new YitiaoBatis();
Yitiao yitiao1 = yitiaoBatis.getYitiao("yitiao");
System.out.println("第一次查询:"+yitiao1);
Yitiao yitiao2 = yitiaoBatis.getYitiao("yitiao");
System.out.println("第二次查询:"+yitiao2);
}
}
输出结果
从结果可以看出:
- 对象创建了一次,有点单例的感觉
- 第一次从数据库查,第二次从缓存查
好像是实现了YitiaoBatis
框架的需求,思考🤔一下有什么问题呢?
4.修改对象id
在测试类继续编写
//执行后续业务,修改id
yitiao2.setId(100);
Yitiao yitiao3 = yitiaoBatis.getYitiao("yitiao");
System.out.println("第三次查询:"+yitiao3);
输出结果
重点看第三次查询,id=100?
我们在内存修改的数据,导致从数据库查出来的数据也跟着改变,出现脏数据。
怎么解决呢?原型模式正式开始。
5.实现Cloneable
接口
本体给外部提供一个克隆体进行使用,在缓存中拿到的对象不直接返回,而是复制一份,这样就保证了不会脏缓存。
public class Yitiao implements Cloneable{
//……
@Override
protected Object clone() throws CloneNotSupportedException {
return (Yitiao) super.clone();
}
}
修改缓存
//从缓存拿对象
public Yitiao getYitiao(String name) throws CloneNotSupportedException {
//判断缓存中是否存在
if (yitiaoCache.containsKey(name)){
Yitiao yitiao = yitiaoCache.get(name);
System.out.println("从缓存查到数据:"+yitiao);
//修改返回
//return yitiao;
return yitiao.clone();
}else {
//模拟从数据库查数据
Yitiao yitiao = new Yitiao();
yitiao.setName(name);
yitiao.setId(1);
yitiao.setWechat("公众号:一条coding");
System.out.println("从数据库查到数据:"+yitiao);
//放入缓存
yitiaoCache.put(name,yitiao);
//修改返回
//return yitiao;
return yitiao.clone();
}
6.再次测试
不用改测试类,直接看一下结果:
从输出结果可以看出第三次查询id
依然是1
,没有脏缓存现象。
基于原型模式的克隆思想,我可以快速拿到和「本体」一模一样的「克隆体」,而且对象也只被new
了一次。
不知道大家是否好奇对象是怎么被创建出来的,那我们就一起看一下「深拷贝」和「浅拷贝」是怎么回事。
深拷贝和浅拷贝
定义
深拷贝:不管拷贝对象里面是基本数据类型还是引用数据类型都是完全的复制一份到新的对象中。
浅拷贝:当拷贝对象只包含简单的数据类型比如int、float 或者不可变的对象(字符串)时,就直接将这些字段复制到新的对象中。而引用的对象并没有复制而是将引用对象的地址复制一份给克隆对象。
好比两个兄弟,深拷贝是年轻的时候关系特别好,衣服买一样的,房子住一块。浅拷贝是长大了都成家立业,衣服可以继续买一样的,但房子必须要分开住了。
实现
在代码上区分深拷贝和浅拷贝的方式就是看引用类型的变量在修改后,值是否发生变化。
浅拷贝
1.通过clone()
方式的浅拷贝
新建Age
类,作为Yitiao
的引用属性
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Age {
private int age;
}
2.测试1
public static void main(String[] args) throws CloneNotSupportedException {
Yitiao yitiao1 = new Yitiao();
Age age = new Age(1);
yitiao1.setAge(age);
yitiao1.setId(1);
Yitiao clone = yitiao1.clone();
yitiao1.setId(2);
age.setAge(2); //不能new一个age
System.out.println("yitiao1:\n"+yitiao1+"\nclone:\n"+clone);
}
输出结果
结论:基本类型id
没发生改变,引用类型Age
由于地址指向的同一个对象,值跟随变化。
3.通过构造方法实现浅拷贝
Yitiao.class
增加构造方法
public Yitiao(Yitiao yitiao){
id=yitiao.id;
age=yitiao.age;
}
4.测试2
Yitiao yitiao1 = new Yitiao();
Age age = new Age(1);
yitiao1.setAge(age);
yitiao1.setId(1);
Yitiao clone = new Yitiao(yitiao1); //差别在这
yitiao1.setId(2);
age.setAge(2);
System.out.println("yitiao1:\n"+yitiao1+"\nclone:\n"+clone);
输出结果
与测试1无异
深拷贝
1.通过对象序列化实现深拷贝
通过层次调用clone方法也可以实现深拷贝,但是代码量太大。特别对于属性数量比较多、层次比较深的类而言,每个类都要重写clone方法太过繁琐。一般不使用,亦不再举例。
可以通过将对象序列化为字节序列后,默认会将该对象的整个对象图进行序列化,再通过反序列即可完美地实现深拷贝。
Yitiao
和Age
实现Serializable接口
2.测试
//通过对象序列化实现深拷贝
Yitiao yitiao = new Yitiao();
Age age = new Age(1);
yitiao.setAge(age);
yitiao.setId(1);
ByteArrayOutputStream bos=new ByteArrayOutputStream();
ObjectOutputStream oos=new ObjectOutputStream(bos);
oos.writeObject(yitiao);
oos.flush();
ObjectInputStream ois=new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()));
Yitiao clone = (Yitiao) ois.readObject();
yitiao.setId(2);
age.setAge(2);
System.out.println("yitiao:\n"+yitiao+"\nclone:\n"+clone);
输出结果
结论,引用对象也完全复制一个新的,值不变化。
不过要注意的是,如果某个属性被transient
修饰,那么该属性就无法被拷贝了。
应用场景
我们说回原型模式。
原型模式在我们的代码中是很常见的,但是又容易被我们所忽视的一种模式,比如我们常用的的
BeanUtils.copyProperties
就是一种对象的浅拷贝。看看有哪些场景需要原型模式
- 资源优化
- 性能和安全要求
- 一个对象多个修改者的场景。
- 一个对象需要提供给其他对象访问,而且各个调用者可能都需要修改其值时可以考虑使用原型模式拷贝多个对象供调用者使用。
原型模式已经与 Java 融为浑然一体,可以随手拿来使用。
总结
原型模式应该算是除了单例最简单的设计模式,但我还是写了将近4个小时,画图,敲代码,码字,不知不觉写了8000
字。
一篇优质的原创文真的很耗费作者的心血,所以如果感觉写的还不错,麻烦给个三连,这对一条来说很重要,也是一条创作下去的动力!
最后
古语云:乘众人之智,则无不任也;用众人之力,则无不胜也。一个人或许可以走的很快,但一群人才能走的更远。
为此,我制定了抱团生长计划,每天分享1-3篇优质文章和1道leetcode算法题
如果你刚刚大一,每天坚持学习,你将会至少比别人多看4000篇文章,多刷1200道题,那么毕业时你的工资就可能是别人的3-4倍。
如果你是职场人,每天提升自己,升职加薪,成为技术专家指日可待。
只要你愿意去奋斗,始终走在拼搏的路上,那你的人生,最坏的结果,也不过是大器晚成。
点此加入计划
如果链接被屏蔽,或者有权限问题,可以私聊作者解决。
粉丝专属福利
📚Java:1.5G学习资料——回复「资料」
📚算法:视频书籍——回复「算法」