设计模式(四):原型模式
什么是原型模式?为什么要使用原型模式?
前两天面试了一个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 来实现,原型模式 好像没有多少用武之地
但是,用的少不代表没用。我们在学习设计模式时,目的不仅仅在于要学会设计模式,而是要学会设计模式使用的设计思想
学会这种思想,沉淀为自己的思路,在工作中能实现举一反三,才能无往不利
-- 以上内容来自公众号 「赫连小伍」 ,转载请注明出处
浙公网安备 33010602011771号