观前须知:
- 笔者还没开始学习JVM,对本文可能会有部分内容仅仅是笔者猜想,会以下划线公示。
- get、set、实例化方法、toString方法均通过lombok提供的注解完成,降低文章的长度
我有个需求!
先不讲什么是原型模式,先从需求出发。现在A厂商需要制作5只黑色笔,每支笔都是黑色的。
很简单!需要创建两个实体类并通过new实例化5只笔。那么看下代码。
//笔的实体类
public class Pen{
private String color;
private Company company;
}
//公司的实体类
public class Company {
private String name;
}
//测试
public class Test {
public static void main(String[] args) {
Pen pen1 = new Pen("黑色",new Company("XXX有限公司"));
Pen pen2 = new Pen("黑色",new Company("XXX有限公司"));
Pen pen3 = new Pen("黑色",new Company("XXX有限公司"));
Pen pen4 = new Pen("黑色",new Company("XXX有限公司"));
Pen pen5 = new Pen("黑色",new Company("XXX有限公司"));
}
}
那么我想问问你几个问题
- 如果我要创建十万字笔怎么提高性能
- 如果我不告诉你这个笔参数是什么,让你弄出来个一摸一样的你该怎么办?
你猜你可能会这么想:
//想法1、
Pen newPen = 未知的Pen
CV 十万次
带着你的问题与我的问题让我们正式开始学习原型模式
什么是原型模式
拷贝一句来自菜鸟教程的话:原型模式(Prototype Pattern)是用于创建重复的对象,同时又能保证性能。
通过这句话,我提出的问题已经得到解决。通过原型模式就能解决因为重复创建并且不丢失性能。
那么让我们开始学习原型模式的两个分支
- 浅拷贝
- 深拷贝
堆与栈
在正式开始学习之前让我们先补充一个知识。
在Java中,基础数据是保存在栈内存中的,栈每次开辟的空间都比较小,只能保存一些比较小的数据。大的数据都会保存在堆内存当中,栈仅仅保存对应堆内存数据的地址。
浅拷贝
从原型的概念可知,我们的目的其实就是复制。那么就对一开始的需求进行修改。我们需要用到JDK提供的一个接口Cloneable并重写Clone方法,就可以完成克隆的操作。让我们看看代码
//pen实体类
public class Pen implements Cloneable{
private String color;
private Company company;
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
//测试
public class Test {
public static void main(String[] args) throws CloneNotSupportedException {
Pen pen1 = new Pen("黑色",new Company("XXX有限公司"));
Pen pen2 = (Pen) pen1.clone();
System.out.println(pen1);//Pen(color=黑色, company=Company(name=XXX有限公司))
System.out.println(pen2);//Pen(color=黑色, company=Company(name=XXX有限公司))
System.out.println(pen1 == pen2);//false
}
}
//====================结果======================
Pen(color=黑色, company=Company(name=XXX有限公司))
Pen(color=黑色, company=Company(name=XXX有限公司))
//====================结束======================
小剧场:很高兴,我们实现了我们的需求。并且地址不一样说明是开辟了两个空间。但是当你开开心心使用这个方法与你懵懵懂懂的朋友炫耀的时候突然你朋友问为啥我修改pen1的公司信息,pen2公司信息也变了,这样我怎么偷偷带走这只笔?你很尴尬你落下一句让我回去研究研究解决下这个问题。
//测试代码
public class Test {
public static void main(String[] args) throws CloneNotSupportedException {
Pen pen1 = new Pen("黑色",new Company("XXX有限公司"));
Pen pen2 = (Pen) pen1.clone();
pen1.setColor("红色");
System.out.println("修改颜色后pen1的信息:"+pen1);
System.out.println("修改颜色后pen2的信息:"+pen2);
pen1.getCompany().setName("YYY有限公司");
System.out.println("修改公司名称后pen1的信息:"+pen1);
System.out.println("修改公司名称后pen1的信息:"+pen2);
}
}
//====================结果====================
修改颜色后pen1的信息:Pen(color=红色, company=Company(name=XXX有限公司))
修改颜色后pen2的信息:Pen(color=黑色, company=Company(name=XXX有限公司))
修改公司名称后pen1的信息Pen:(color=红色, company=Company(name=YYY有限公司))
修改公司名称后pen1的信息Pen:(color=黑色, company=Company(name=YYY有限公司))
//====================结束====================
真如你所见,明明已经是两个地址不相同的对象为什么会出现这种情况?这个时候就需要前面提到的补充知识堆栈。【这里的知识就有点不是很确定,知识通过我学过的知识进行猜测】
看图,其实我们通过clone()方法克隆的其实仅仅是堆内存的一片地址空间。然后找一个空闲的栈内存区域进行原模原样的复制。将保存在堆内存的地址信息也一同复制,所以就出现了我们修改基础参数各是各的,但是修改引用类型Company类就会连锁修改的假象。【不太确定】
那么上面这个例子你懂了吗?其实这就是浅拷贝。相信各位已经有了自己的定义。
深拷贝
了解了浅拷贝但是会发现,这并不是我们想要的,我们要的是各是各的不要这种奇奇怪怪的。那么深拷贝来了。但是我不想告诉直接告诉你们代码,想让你们和我一起想怎么解决!
方法一
根据我画的图发现浅拷贝其实就是复制栈内存的数据,那么把堆内存的数据也拷贝一份不就完了。对这就是深拷贝的一种解决思路,让我们看看代码
//公司实体类
public class Company implements Cloneable{
private String name;
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
//Pen实体类
public class Pen implements Cloneable{
private String color;
private Company company;
@Override
protected Object clone() throws CloneNotSupportedException {
//先克隆栈内存
Pen pen = (Pen) super.clone();
//再克隆堆内存
pen.company = (Company) company.clone();
return pen;
}
}
//测试代码------->与之前一摸一样
public class Test {
public static void main(String[] args) throws CloneNotSupportedException {
Pen pen1 = new Pen("黑色",new Company("XXX有限公司"));
Pen pen2 = (Pen) pen1.clone();
pen1.setColor("红色");
System.out.println("修改颜色后pen1的信息:"+pen1);
System.out.println("修改颜色后pen2的信息:"+pen2);
pen1.getCompany().setName("YYY有限公司");
System.out.println("修改公司名称后pen1的信息"+pen1);
System.out.println("修改公司名称后pen1的信息"+pen2);
}
}
//=======================结果=======================
修改颜色后pen1的信息:Pen(color=红色, company=Company(name=XXX有限公司))
修改颜色后pen2的信息:Pen(color=黑色, company=Company(name=XXX有限公司))
修改公司名称后pen1的信息Pen(color=红色, company=Company(name=YYY有限公司))
修改公司名称后pen1的信息Pen(color=黑色, company=Company(name=XXX有限公司))
//=======================结束=======================
方法二
别急别急别急,虽然问题已经解决了,但是有一个更棒的代码你不想看一下吗?我们还可以通过序列化和反序列化进行深拷贝哦。让我们直接看代码【原因是笔者对序列化不是很懂】
//先让company与Pen继承Serializable哦
public class Pen implements Serializable{
private String color;
private Company company;
//为了节省空间,省略try-catch,以及关闭流等代码规范
public Object deepClone(){
//序列化
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(this);
//反序列化
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis);
Pen pen = (Pen) ois.readObject();
return pen;
}
}
卑微的阐述一下笔者的想法:序列化就是将对象转化成比特流保存在内存中,内存就必须开辟新的空间。然后通过反序列化将内存的数据又进行创建,数据还是一样的,但是内存空间却变化了。
好像还有个问题?
到了这里其实大家已经学会了原型模式,但是还记不记得最开始我们有个问题就是直接通过=进行赋值做到复制。那么我们来测试一下
//测试代码
public class Test {
public static void main(String[] args){
Pen pen1 = new Pen("黑色",new Company("XXX有限公司"));
Pen pen2 = pen1;
System.out.println(pen1 == pen2);
}
}
//===================结果===================
true
//===================结束===================
可见其实两个的地址时相同的,那么我们修改值会导致所有的都进行变化。所以这样的复制也是不完成不了需求的
总结
此时此刻,问题也解决了也学习到了原型模式。那么就要对他进行总结。
优点:
- 提高创建的性能
- 简化创建的复杂度
- 不需要知道对象本身就可以获取一个相同的对象
- 对初始的代码进行修改,克隆的对象也会对应的修改。不需要修改代码
缺点:
- 不遵循开闭原则OCP
最后配上一个静态池和=赋值的地址空间图【不一定对哦,仅供观摩不能学习】
参考文档:原型模式 | 菜鸟教程
笔者听说面试可以再上面写自己博客的地址让面试官看看,所以这是笔者第一次写博客,如果有错麻烦各位大佬提点我好及时修改