什么是原型模式?为什么要使用原型模式?
前两天面试了一个95年硕士毕业的小姐姐,在杭州某大厂工作了两年,最近想回家乡发展
对于两年以上工作经验的候选人,我都会问一些和设计模式相关的面试题
不得不面对一个现实,大部分候选人对设计模式都没有很深入的理解,回答的并不出彩
当我对这个小姐姐提出这两个问题时,也没抱有很高的期望。没想到小姐姐的回答很让人意外,甚至可以说是让我对原型模式有了更深刻的理解
为什么要使用原型模式
假如有一个类,命名为A。A类里面有两个属性,分别是x和y,并为这两个属性提供对应的get和set方法
将这个类的实体对象a作为test方法的参数
要求在test方法内利用a对象的某些属性进行一些业务逻辑处理,但不能改变a对象的原有属性
我们进行第一次尝试:声明一个新的对象a1,并把a赋值给它。让test方法利用a1对象的属性进行业务逻辑处理
publicstaticvoidtest(A a){
A a1 = a;
System.out.println("test方法开始业务逻辑处理");
a1.setX(1);
}
我们来验证一下是否会影响到a对象的属性
publicstaticvoidmain(String[] args){
A a =newA();
a.setX(0);
System.out.println("调用test方法前x="+ a.getX());
test(a);
System.out.println("调用test方法后x="+ a.getX());
}
输出结果为
从输出结果来看,test方法改变了a对象的属性,不符合要求。所以,第一次尝试失败
其实也不难理解,我们都知道JVM加载对象后会给对象分配内存空间
加载完a之后,给a分配一个空间
在加载a1的时候,因为a1是将a的值赋值给了a1,所以在给a1分配空间时,只是把a1的引用指向了a所在的内存地址,并没有给a1分配独立的内存空间
所以修改a1对象的属性时,a对象也会被改变
我们调整思路进行第二次尝试:重新new一个新对象a2,把a对象的所有属性值赋值给a2。test方法利用a2对象进行业务逻辑处理
publicstaticvoidtest(A a){
A a2 =newA();
a2.setX(a.getX());
a2.setY(a.getY());
System.out.println("test方法开始业务逻辑处理");
a2.setX(1);
a2.setY(2);
}
同样来验证一下是否会影响到a对象的属性
publicstaticvoidmain(String[] args){
A a =newA();
a.setX(0);
a.setY(0);
System.out.println("调用test方法前x="+ a.getX() +",y="+ a.getY());
test(a);
System.out.println("调用test方法后x="+ a.getX() +",y="+ a.getY());
}
输出结果为
这次的输出结果显示,test方法并没有改变a对象的属性,符合要求
但是,有一个问题
如果a不是一个具体的实例,而是一个抽象类或者接口。抽象类或者接口是不能被new的,该怎么办?
这时候就要使用到原型模式来解决我们的问题了
原型模式
原型模式定义
「原型模式」可以让你复制或克隆一个已有对象,而又无需使你的代码依赖这个对象所属的类
通过定义我们可以提取出来两个关键信息
第一,原型模式主要作用是复制或克隆一个已有对象
第二,去复制这个对象时不需要依赖这个对象所属的类
这句话很有意思,想要创建一个对象但是不用依赖这个对象所属的类,这要怎么实现?
答案就是把创建对象的过程交给这个类来处理
原型模式实战
我们用原型模式来优化一下上面的例子
动手之前我们需要知道原型模式的设计思路
根据定义可以知道在原型模式中,对象的创建过程是交给对象所属的类来处理的,所以这个类肯定要提供一个方法,方法的返回值是这个对象。通常这个方法叫clone()或copy()
套用到上面例子的A类中,需要在A类里面提供一个clone()方法,在方法中创建一个当前对象并返回
在test方法中利用clone()来获取一个a3对象去执行业务逻辑
publicstaticvoidtest(A a){
A a3 = a.clone();
System.out.println("test方法开始业务逻辑处理");
a3.setX(1);
}
再验证一下是否改变了a对象的属性
从输出结果可以看到是没有改变a对象的属性的
那我们再来解决上面例子中遇到的问题,假如A是一个抽象类,该怎么去创建这个对象
其实也很简单,抽象类中是可以有抽象方法的。把clone()方法定义为抽象方法,让子类去实现它
假如A有两个子类,分别是SubA1和SubA2。两个子类分别继承A抽象类,并实现clone()抽象方法
在test中还是使用a.clone()就可以得到一个新的对象,而且不会影响到原有的a对象
这就用原型模式对上面的例子完成了优化
深拷贝、浅拷贝
在java中,默认Object类是所有类的父类,在Object中有一个clone()方法
它是java默认提供的用来复制对象的方法,这个方法是native修饰的,说明它是对操作系统的底层直接调用的,在理论上,用它来复制对象性能会更好
所以,我们可以使用java.lang.Object#clone()来实现原型模式,总共分为两步
被复制的类需要实现Cloneable这个接口类。这个接口类里面是没有任何一个方法的,只是起到一个标记作用,也可以理解成一种约定(「约定大于配置」)
被复制的类需要重写Object中的clone()方法
下面我们就来优化一下A类
这样写出的原型模式,在理论上执行效率更高。看似完美,实则不然
假如A类里面有一个ArrayList属性
我们来看一下,在clone完a后得到a4,改变a4的list属性,会不会对a造成影响
输出结果为
在修改a4对象时,也改变了a对象的属性值,这不是我们期望的结果
这是因为:Object在clone时只会对基础类型的数据进行拷贝,引用类型的数据并没有真正的拷贝,而是把引用指针指向了这个数据在内存中的地址(还记得上文中a和a1指向同一个内存地址的例子吗)
这种只拷贝基础数据类型的行为,我们称之为浅拷贝。既可以拷贝基础数据类型,又可以拷贝引用数据类型的行为,我们称之为深拷贝
在原型模式中,我们应该使用深拷贝来复制对象。
要实现深拷贝,「需要这个引用类型的数据所属的类也实现Cloneable接口,并且重写Object类的clone()方法」
在本例子中,引用类型所属的类是ArrayList,它本身已经实现了Cloneable接口,并重写了Object类的clone()方法。
我们只需要在A类的clone()方法中调用ArrayList的clone()方法即可
这样就基于深拷贝完成了原型模式
总结
「原型模式」也叫「克隆模式」,它属于设计模式三大类型中的创建型模式
在你需要复制一个对象,而又不希望改变原有对象的时候可以考虑使用原型模式来实现
在实现原型模式时,引用类型数据的复制要基于深拷贝,否则会影响到被拷贝的原型
在Spring生态下,对象的创建基本都由IOC来实现,原型模式好像没有多少用武之地
但是,用的少不代表没用。我们在学习设计模式时,目的不仅仅在于要学会设计模式,而是要学会设计模式使用的设计思想
学会这种思想,沉淀为自己的思路,在工作中能实现举一反三,才能无往不利
-- 以上内容来自公众号「赫连小伍」,转载请注明出处