什么是原型模式?为什么要使用原型模式?
前两天面试了一个95年硕士毕业的小姐姐,在杭州某大厂工作了两年,最近想回家乡发展
对于两年以上工作经验的候选人,我都会问一些和设计模式相关的面试题
不得不面对一个现实,大部分候选人对设计模式都没有很深入的理解,回答的并不出彩
当我对这个小姐姐提出这两个问题时,也没抱有很高的期望。没想到小姐姐的回答很让人意外,甚至可以说是让我对原型模式有了更深刻的理解
为什么要使用原型模式
假如有一个类,命名为 A
。A
类里面有两个属性,分别是 x
和 y
,并为这两个属性提供对应的 get
和 set
方法
将这个类的实体对象 a
作为 test
方法的参数
要求在 test
方法内利用 a
对象的某些属性进行一些业务逻辑处理,但不能改变 a
对象的原有属性
我们进行第一次尝试:声明一个新的对象 a1
,并把 a
赋值给它。让 test
方法利用 a1
对象的属性进行业务逻辑处理
public static void test(A a) {
A a1 = a;
System.out.println("test方法开始业务逻辑处理");
a1.setX(1);
}
我们来验证一下是否会影响到 a
对象的属性
public static void main(String[] args) {
A a = new A();
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
对象进行业务逻辑处理
public static void test(A a) {
A a2 = new A();
a2.setX(a.getX());
a2.setY(a.getY());
System.out.println("test方法开始业务逻辑处理");
a2.setX(1);
a2.setY(2);
}
同样来验证一下是否会影响到 a
对象的属性
public static void main(String[] args) {
A a = new A();
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
对象去执行业务逻辑
public static void test(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
来实现,原型模式
好像没有多少用武之地
但是,用的少不代表没用。我们在学习设计模式时,目的不仅仅在于要学会设计模式,而是要学会设计模式使用的设计思想
学会这种思想,沉淀为自己的思路,在工作中能实现举一反三,才能无往不利
-- 以上内容来自公众号 「赫连小伍」 ,转载请注明出处