原型模式也是创建型模式的一种,关于原型模式其实应用的非常广泛,例如spring中的Bean的@Scope(“prototype”)注解,JSON.parseObject() 等都是原型模式的具体应用。今天我们就来学习学习一下原型模式。
1、什么是原型模式
-
在开发中有时候我们回碰到这种问题
(1)一个对象创建的代价很大,在创建该对象前需要大量的准备工作,同时这个对象还要多次创建;
(2)多个对象之前区别不大,由于没有好的办法,任然要多次创建;
(3)有一个对象需要被大量使用,同时在不同的使用场景需要给属性重新赋值;
在以上的情形中,我们都是要对一个类(class)进行多次实例化(上述1和2),或者不断的修改一个对象(上述3)。问题有首先比较操作比较麻烦,在一个影响性能。有什么方法可以解决上述场景的问题?那就是原型模式。 -
所谓原型其实就是一个类的对象,根据这个对象,我们就可以通过拷贝这个对象,获得这个对象对应类的其他对象,比如:有个class A,想要获得该类的对象就要A a = new A(),把a当作原型,通过a我们就可以获得A的多个实例。通过a实例如何拷贝并获取到A的其他实例,这就是原型模式要做的事情。
-
原型模式的定义:
用一个已经创建的实例作为原型,通过复制该原型对象来创建一个和原型相同或相似的新对象。
在这里,原型实例指定了要创建的对象的种类。用这种方式创建对象相对于new操作来说非常高效,根本无须知道对象创建的细节。例如,Windows 操作系统的安装通常较耗时,如果复制就快了很多。 -
原型模式的结构
(1)原型模式包含以下主要角色。
抽象原型类:规定了具体原型对象必须实现的接口。
具体原型类:实现抽象原型类的 clone() 方法,它是可被复制的对象。
访问类:使用具体原型类中的 clone() 方法来复制新的对象。
(2)类图
2、原型模式的实现
下面通过一个实例,展示原型模式的实现:有一个图形类,其中一个子类是圆形类,现在需要在多处使用原型的实例,使用原型模式如何做。
2.1 首先设计类图:
Shape.java
public abstract class Shape implements Cloneable {
private Integer id;
private String type;
/**
* 画图
*/
abstract void draw();
/**
* 克隆方法
* @return Shape的实例
*/
@Override
public Shape clone(){
Shape shape = null;
try {
shape = (Shape) super.clone();
} catch (Exception e) {
e.printStackTrace();
}
return shape;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
}
Circle.java
public class Circle extends Shape {
public Circle() {
try {
//进行休眠2s
System.out.println("休眠2s,模拟复杂的创建过程");
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.setType("圆形");
}
@Override
void draw() {
System.out.println("我是圆的!");
}
}
ShapeManager.java
public class ShapeManager {
private static Map<Integer,Shape> shapeCache = new HashMap<>();
public static Shape getShape(Integer id){
return shapeCache.get(id);
}
/**
* 模拟对象创建前要进行诸多工作,并将对象缓存起来,后边使用可以直接利用原型对象复制获取新的对象
*/
public static void loadShape(){
try {
//进行休眠2s
System.out.println("先休眠2s,模拟创建前的准备工作");
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
Circle circle = new Circle();
circle.setId(1);
shapeCache.put(circle.getId(),circle);
}
}
Client.java
public class Client {
public static void main(String[] args) {
ShapeManager.loadShape();
Shape circle1 = ShapeManager.getShape(1);
Shape circle2 = circle1.clone();
Shape circle3 = circle1.clone();
circle1.draw();
circle2.draw();
circle3.draw();
}
}
输出:
说明:
1、相比标准的原型模式这里多了一个管理类,作用在类图的注释里已经说明了。这个类可以不用,直接在客户端中new一个对象,然后再通过new来的对象复制获取新对象就行了。
2、抽象Shape类中的clone方法,shape = (Shape) super.clone();
,这是实现原型模式的关键:
super自然就是Shape的父类,也就是Object类的clone(),关于此方法的使用说明在jdk中说的很清楚,可以点进去看看。
简单说就是:
任何对象都可以通过他的clone()
方法(从Object继承而来)获得一个对象的副本,这个副本独立于拷贝对象,可以修改拷贝的副本对象的字段值。同时任何对象想要使用clone()
,必须首先实现Cloneable
接口,这是个标记接口内部没有任何方法,如果不实现该接口,调用clone就会抛出CloneNotSupportedException
异常,并且clone()
实现的是一种浅复制,关于深浅复制我们下一节在做说明。
3、在测试代码中我们获取了三个Circle对象,一个时从缓存中获取,另外两个时复制得来的,从输出可以看到只有在第一个对象创建之前进行了对象创建前的准备工作,后两个是很快的。由此可以说明,拷贝的效率比新创建要好的多。
3、浅复制和深复制
3.1 浅复制
浅复制就是上述使用clone()方法实现的对象复制,特点是被复制对象的所有值属性都含有与原来对象的相同,而所有的对象引用属性仍然指向原来的对象。例如,我们对上述的原型模式实现代码做如下修改:
1、添加Point类,为Circle类添加一个Point类型的成员变量作为圆心,并且再draw()
方法中打印圆心。
Point.java
public class Point {
/** x/y坐标 */
private Double x;
private Double y;
public Point(Double x, Double y) {
this.x = x;
this.y = y;
}
public Double getX() {
return x;
}
public void setX(Double x) {
this.x = x;
}
public Double getY() {
return y;
}
public void setY(Double y) {
this.y = y;
}
}
Circle.java
public class Circle extends Shape {
private Point point = null;
public Point getPoint() {
return point;
}
public void setPoint(Point point) {
this.point = point;
}
public Circle() {
try {
//进行休眠2s
System.out.println("休眠2s,模拟复杂的创建过程");
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.setType("圆形");
}
@Override
void draw() {
System.out.println("我是"+this.getType()+"的!我的圆心坐标是("+point.getX()+","+point.getY()+")");
}
}
2、再ShapeManager 类中创建Circle对象时设置圆心为(0,0)
ShapeManager .java
public class ShapeManager {
private static Map<Integer,Shape> shapeCache = new HashMap<>();
public static Shape getShape(Integer id){
return shapeCache.get(id);
}
/**
* 模拟对象创建前要进行诸多工作,并将对象缓存起来,后边使用可以直接利用原型对象复制获取新的对象
*/
public static void loadShape(){
try {
//进行休眠2s
System.out.println("先休眠2s,模拟创建前的准备工作");
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
Circle circle = new Circle();
circle.setId(1);
circle.setPoint(new Point(0.0,0.0));
shapeCache.put(circle.getId(),circle);
}
}
3、Client类修改如下
Client.java
public class Client {
public static void main(String[] args) {
ShapeManager.loadShape();
Circle circle1 = (Circle)ShapeManager.getShape(1);
Circle circle2 = (Circle)circle1.clone();
Circle circle3 = (Circle)circle1.clone();
//查看三个对象中point变量的地址
System.out.println(circle1.getPoint());
System.out.println(circle2.getPoint());
System.out.println(circle3.getPoint());
circle1.draw();
//修改circle2中的point值
Point point = circle2.getPoint();
point.setX(1.0);
point.setY(1.0);
circle2.draw();
circle3.draw();
}
}
4、Shape类不做修改
执行Client.java,输出如下:
可以看到,副本对象和原对象的point变量是指向了同一个地址,这样修改circle2的point值,circle3的point值就也变了,这显然是违背我们意愿的,我i们希望副本对象和拷贝对象是完全独立的。
以上就是浅复制的特点。
3.2 深复制
有了上面对浅复制的说明,深复制就很容易理解了,即:在浅拷贝的基础上,所有引用其他对象的变量也进行了clone,并指向被复制过的新对象
4、原型模式中的深复制
那么原型模式怎样实现深复制呢?其实非常简单,再clone()
中对point进行单独处理,同样使用point.clone()获取一个point对象的副本,并赋值到circle对象。对3.1中原型模式代码做如下修改:
1、Point类实现Cloneable接口,并重写clone方法
Point.java
public class Point implements Cloneable {
/**
* x/y坐标
*/
private Double x;
private Double y;
public Point(Double x, Double y) {
this.x = x;
this.y = y;
}
@Override
public Point clone() {
Point p = null;
try {
p = (Point) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return p;
}
public Double getX() {
return x;
}
public void setX(Double x) {
this.x = x;
}
public Double getY() {
return y;
}
public void setY(Double y) {
this.y = y;
}
}
2、 Shape类中clone方法做如下修改
Shape.jva
public abstract class Shape implements Cloneable {
private Integer id;
private String type;
/**
* 画图
*/
abstract void draw();
/**
* 克隆方法
* @return Shape的实例
*/
@Override
public Shape clone(){
Shape shape = null;
try {
shape = (Shape) super.clone();
if (shape instanceof Circle){
Circle circle = (Circle) shape;
circle.setPoint(circle.getPoint().clone());
}
} catch (Exception e) {
e.printStackTrace();
}
return shape;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
}
执行Client中的代码,输出如下:
可见,三个Circle对象指向的地址都不一样,同时修改circle2的point值,并不会影响circle3,这样我们就实现了深复制!
5、JDK中原型模式的使用
1、一般只要是实现了Cloneable接口都是使用了原型模式。比如我们找到ArrayList的源码,类结构如下:
2、可以看到ArrayList实现了Cloneable接口,那么必然会有一个clone方法,找到它,实现如下:
可以看到,是浅复制。只是把数组元素赋值了一遍。
6、补充:使用序列化的方式实现深复制
原理非常简单,就是将对象序列化后,通过反序列化获得新的对象,完成深拷贝。
1、修改Client代码如下
Client.java
public class Client {
public static void main(String[] args) {
Circle circle1 = new Circle();
circle1.setId(1);
circle1.setType("原型");
circle1.setPoint(new Point(0.0,0.0));
try(ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos)){
//序列化
oos.writeObject(circle1);
try(ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis)){
//反序列化
Circle circle2 = (Circle) ois.readObject();
//重置circle的point
circle2.setPoint(new Point(1.0,1.0));
circle1.draw();
circle2.draw();
}
}catch (Exception e){
e.printStackTrace();
}
}
}
2、修改Point类和Circle类,使之继承Serializable接口,如下图
Point.java
Circle.java
执行Client的代码,输出如下:
可见实现了深复制。为什么type为null呢?原因是type属性是继承来的属性,它再Shape类中,Shape类没有继承序列化接口,修改Shape类使之继承序列化接口,重新测试。
重新执行Client的代码,输出如下: