原型模式
定义:用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。
是用于创建重复的对象,同时又能保证性能。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。这种模式是实现了一个原型接口,该接口用于创建当前对象的克隆。当直接创建对象的代价比较大时,则采用这种模式。例如,一个对象需要在一个高代价的数据库操作之后被创建。我们可以缓存该对象,在下一个请求时返回它的克隆,在需要的时候更新数据库,以此来减少数据库调用。
原型模式的优缺点
优点:
- 原型模式性能比直接new一个对象性能高,是在内存中二进制流的拷贝,要比直接new一个对象性能好很多,特别是要在一个循环体内产生大量对象时,原型模式可能更好的体现其优点。
- 简化创建过程。
- 保护性拷贝,也就是对某个对象对外可能是只读的,为了防止外部对这个只读对象的修改,通常可以通过返回一个对象拷贝的形式实现只读的限制。
缺点:
- 首先要记住原型模式的拷贝时不会执行构造函数的。
- clone并不一定比new一个对象快,只有当new对象比较耗时时,才考虑使用原型模式。
- 必须配备克隆方法。
- 对克隆复杂对象或对克隆出的对象进行复杂改造时,容易引入风险。
- 深拷贝、浅拷贝要运用得当。
- 要使用clone方法,类的成员变量上不要增加final关键字,final类型是不允许重赋值的。
原型模式的应用场景
- 对象之间相同或相似,即只是个别的几个属性不同的时候。
- 对象的创建过程比较麻烦,但复制比较简单的时候。
原型模式的实现
原型模式主要用于对象的复制,它的核心是就是类图中的原型类Prototype。Prototype类需要具备以下两个条件:
- 实现Cloneable接口。在java语言有一个Cloneable接口,它的作用只有一个,就是在运行时通知虚拟机可以安全地在实现了此接口的类上使用clone方法。在java虚拟机中,只有实现了这个接口的类才可以被拷贝,否则在运行时会抛出CloneNotSupportedException异常。
- 重写Object类中的clone方法。Java中,所有类的父类都是Object类,Object类中有一个clone方法,作用是返回对象的一个拷贝,但是其作用域protected类型的,一般的类无法调用,因此Prototype类需要将clone方法的作用域修改为public类型。
public class Prototype implements Cloneable {
private String name;
private List<String> names;
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
public List<String> getNames() {
return names;
}
public void setNames(List<String> names) {
this.names = names;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
测试
public class Main {
public static void main(String[] args) throws CloneNotSupportedException {
Prototype prototype = new Prototype();
prototype.setName("原型");
List<String> names = new ArrayList<>();
names.add("原型");
prototype.setNames(names);
for (int i = 0; i < 5; i++){
Prototype p = (Prototype) prototype.clone();
p.setName("测试" + i);
p.getNames().add("测试" + i);
System.out.println(p.toString());
System.out.println(p.getName());
System.out.println(p.getNames().size());
}
System.out.println(prototype.toString());
System.out.println(prototype.getName());
System.out.println(prototype.getNames().size());
}
}
输出
com.example.demo.prototypePattern.Prototype@681a9515
测试0
2
com.example.demo.prototypePattern.Prototype@3af49f1c
测试1
3
com.example.demo.prototypePattern.Prototype@19469ea2
测试2
4
com.example.demo.prototypePattern.Prototype@13221655
测试3
5
com.example.demo.prototypePattern.Prototype@2f2c9b19
测试4
6
com.example.demo.prototypePattern.Prototype@31befd9f
原型
6
通过输出结果可以看出:
- 每个对象的地址是不同的
- 修改基本类型时,并不能影响基础类,而引用对象只是指向的基础类的属性。
原型模式对单例模式的破坏
当单例对象实现了clone方法时,会返回多个实例,请看实现:
/**
* 简单的饿汉式单例
*/
public class StaticInnerClassSingleton implements Cloneable {
/**
* 看静态类的初始化锁那个线程可以拿到
*/
private static class InnerClass {
private static StaticInnerClassSingleton staticInnerClassSingleton = new StaticInnerClassSingleton();
}
public static StaticInnerClassSingleton getInstance() {
return InnerClass.staticInnerClassSingleton;
}
private StaticInnerClassSingleton() {
if (InnerClass.staticInnerClassSingleton != null) {
throw new RuntimeException("单例对象禁止反射调用");
}
}
/**
* 直接重写clone方法
* @return
* @throws CloneNotSupportedException
*/
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
public class Test {
public static void main(String[] args) throws CloneNotSupportedException {
//获取单例对象
StaticInnerClassSingleton singleton = StaticInnerClassSingleton.getInstance();
System.out.println(singleton.toString());
//clone获取克隆对象
StaticInnerClassSingleton singleton1 = (StaticInnerClassSingleton) singleton.clone();
System.out.println(singleton1.toString());
}
}
输出日志
com.example.demo.prototypePattern.StaticInnerClassSingleton@1540e19d
com.example.demo.prototypePattern.StaticInnerClassSingleton@677327b6
根据日志可以看出,单例模式被破坏掉。
重写clone()方法,直接返回INSTANCE对象解决原型模式对单例模式的破坏
/**
* 简单的饿汉式单例
*/
public class StaticInnerClassSingleton1 implements Cloneable {
/**
* 看静态类的初始化锁那个线程可以拿到
*/
private static class InnerClass {
private static StaticInnerClassSingleton1 staticInnerClassSingleton = new StaticInnerClassSingleton1();
}
public static StaticInnerClassSingleton1 getInstance() {
return InnerClass.staticInnerClassSingleton;
}
private StaticInnerClassSingleton1() {
if (InnerClass.staticInnerClassSingleton != null) {
throw new RuntimeException("单例对象禁止反射调用");
}
}
/**
* 修改克隆方法,返回单例对象
* @return
* @throws CloneNotSupportedException
*/
@Override
protected Object clone() throws CloneNotSupportedException {
return InnerClass.staticInnerClassSingleton;
}
}
/**
* 修改后的测试类
*/
public class Test1 {
public static void main(String[] args) throws CloneNotSupportedException {
//获取单例对象
StaticInnerClassSingleton1 singleton1 = StaticInnerClassSingleton1.getInstance();
System.out.println(singleton1.toString());
//获取clone对象
StaticInnerClassSingleton1 singleton2 = (StaticInnerClassSingleton1) singleton1.clone();
System.out.println(singleton2.toString());
}
}
输出日志
com.example.demo.prototypePattern.StaticInnerClassSingleton1@1540e19d
com.example.demo.prototypePattern.StaticInnerClassSingleton1@1540e19d
可以看出,返回的对象地址时一致的。这样就解决了原型对单例模式的破坏。
拓展:深拷贝与浅拷贝
浅拷贝:创建一个新对象,然后将当前对象的非静态字段复制到该新对象,如果字段是值类型的,那么对该字段执行复制;如果该字段是引用类型的话,则复制引用但不复制引用的对象。因此,原始对象及其副本引用同一个对象。
深拷贝:创建一个新对象,然后将当前对象的非静态字段复制到该新对象,无论该字段是值类型的还是引用类型,都复制独立的一份。当你修改其中一个对象的任何内容时,都不会影响另一个对象的内容。
实现深拷贝的方法:
- 让每个引用类型属性内部都重写clone() 方法
- 利用序列化,序列化是将对象写到流中便于传输,而反序列化则是把对象从流中读取出来。这里写到流中的对象则是原始对象的一个拷贝,因为原始对象还存在 JVM 中,所以我们可以利用对象的序列化产生克隆对象,然后通过反序列化获取这个对象。注意每个需要序列化的类都要实现 Serializable接口,如果有某个属性不需要序列化,可以将其声明为 transient,即将其排除在克隆属性之外。