什么是原型设计模式?
用原型实例指定创建对象的种类,并通过拷贝这些原型创建包含了原对象中所有信息的新的对象。
原型设计模式在某种条件下,是一种非常危险的,难以驾驭的设计模式。使用应该慎之又慎。
原型设计模式怎么用?
原型设计模式没有复杂的概念,它就是用来拷贝对象的。
在java中若想使一个对象能够被拷贝,基本上有下面这几种方法:
- 实现Cloneable接口。
- 重写clone()方法,并调用super.clone()方法获取到拷贝对象
- 若目标对象包含复杂对象引用,则继续调用引用对象的clone()方法,再将结果set到目标对象上去。
- 针对复杂的,引用较深的对象,使用内存流+序列化的方式,直接获取到完全拷贝对象。
/**
* 原型设计模式
*/
public class Prototype implements Cloneable, Serializable {
private static final long serialVersionUID = 8196154781151609930L;
private ArrayList<String> arrayList = new ArrayList<>();
private Object[] models = {new Object(), new Object()};
// final与clone价架构不兼容。因为你无法在初始化以外的地方改变final的引用
private final Prototype prototype = new Prototype();
/**
* 重写Object的clone方法
*
* @return
* @throws CloneNotSupportedException
*/
@Override
public Object clone() throws CloneNotSupportedException {
Prototype prototype = (Prototype) super.clone();
prototype.arrayList = (ArrayList<String>) this.arrayList.clone();
prototype.models = this.models.clone();
//this.prototype = (Prototype) prototype.clone();
return prototype;
}
/**
* 通过对象序列化进行深拷贝
*
* @return
*/
public Prototype deepClone() throws IOException, ClassNotFoundException {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream outputStream = new ObjectOutputStream(bos);
outputStream.writeObject(this);
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis);
return (Prototype) ois.readObject();
}
}
原型设计模式的设计缺陷和安全隐患
为什么文章开头说,原型设计模式在某种条件下,是一种非常危险的,难以驾驭的设计模式?这需要从Cloneable接口糟糕的设计说起。
首先,Cloneable接口没有包含任何方法,实现它的作用是改变了他的父类中受保护的clone方法的实现行为,使得Object的clone方法能够返回一个拷贝对象。这样的接口设计颠覆了java的接口设计理念。
往往实现一个接口,是为了告诉客户这个类能够为他做些什么。而Cloneable却改变了父类的clone方法的行为。这样极端的做法,基本上可以用匪夷所思来形容。
其次,调用了java语言以外的方法创建了对象,同时深拷贝浅拷贝的问题带来了巨大的安全隐患。比如下面这一段代码:
/**
* 测试
*
* @param args
*/
public static void main(String[] args) {
Prototype prototype = new Prototype();
System.out.println(prototype.models[0]);
Object object1 = prototype.models[0];
try {
Prototype clonePrototype = (Prototype) prototype.clone();
System.out.println(clonePrototype.models[0]);
Object object2 = prototype.models[0];
System.out.println(object1 == object2);
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
}
输出结果
java.lang.Object@63947c6b
java.lang.Object@63947c6b
true
这太可怕了。你不得不仔细检查你的每一个属性,直到你确保他们的clone都得到调用为止。
接着,如果你的类设计为了一个不可变类,或者类中有final字段的话,那么恭喜,clone架构与引用可变对象的final是不兼容的,clone方法无法为final字段赋值。因为你无法在初始化以外的地方重新改变引用。
然后,clone方法“意料之中”的不是同步的。这意味着如果想让你的对象在并发中安全的话,则得花心思在clone方法的同步处理上。而由于clone架构与final设计理念冲突的原因,你又无法将你的类设计为不可变类!
最后,如果你的对象是出于被继承的目的而被设计出来的话,那么你的clone方法总是会被所有子类重写。所以建议被继承的类保持与Object的设计一致,不主动实现Cloneable方法。但是你需要覆盖clone方法,重写clone逻辑,方法保持protected的访问权限。这样做得以让子类拥有是否实现接口的自由,并且还能保证你的类能够拥有正确的“深拷贝”行为。
所以,如果你想要写出健壮,安全,维护性强的代码,你就应该考虑告别Cloneable接口。这个世界上,有些经验丰富的程序员永远不会实现Cloneable接口。
总结
- 原型模式常常和其他设计模式搭配使用。
- 注意深浅拷贝的区别。
- 鉴于Cloneable糟糕的设计,对于任何自定义类,如果你没有充足的把握,请不要实现Cloneable接口。凡是实现了Cloneable接口的自定义类,一定要给用户提供安全的clone实现。
- 如果你的自定义类需要并发安全,请花心思对clone方法进行同步处理。
- 复杂对象建议使用序列化+流的方式重写。这相当安全,并且不容易出错。
网友评论