前言
原型模式很少单独出现,一般都是和工厂方法模式一起出现,它其实就是把对象生成的责任代理给自己,实现了自我复制,达成了对象拷贝的多态。今天简单记录一下原型模式。
目录
一、定义
使用原型实例指定创建对象的种类,然后通过拷贝这些原型来创建新的对象
二、模式原理分析
使用者 需要建立一个原型,才能基于原型拷贝出新实例,还需要决策什么时候使用原型、什么时候使用新实例以及从原型到新实例之间的拷贝应该采用什么样的算法策略
查看如下代码
public interface PrototypeInterface extends Cloneable {
PrototypeInterface clone() throws CloneNotSupportedException;
}
public class ProtypeA implements PrototypeInterface {
@Override
public ProtypeA clone() throws CloneNotSupportedException {
System.out.println("Cloning new object: A");
return (ProtypeA) super.clone();
}
}
public class ProtypeB implements PrototypeInterface {
@Override
public ProtypeB clone() throws CloneNotSupportedException {
System.out.println("Cloning new object: B");
return (ProtypeB) super.clone();
}
}
//ProtypeA以自己为原型通过拷贝创建一个新的对象newInstanceA
public static void main(String[] args) throws CloneNotSupportedException {
ProtypeA source = new ProtypeA();
System.out.println(source);
ProtypeA newInstanceA = source.clone();
System.out.println(newInstanceA);
}
复制代码
原型模式封装了如下变化:
-
原始对象的构造方式,clone 方法的原理是从内存种以二进制流的方式进行拷贝,重新分配内存块,所以构造函数不会执行
-
对象的属性与其他对象间的依赖关系
-
对象运行时状态的获取方式
-
对象拷贝的具体实现策略 clone 方法的实现
所以说,原型模式从建立原型到拷贝原型生成新实例,都是对用户透明的,一旦中间任何一个小细节出现问题,你可能获取的就是一个错误的对象。
三、原型拷贝的方式
浅拷贝
public class Test implements Cloneable{
private ArrayList<String> arrayList = new ArrayList<>();
@Override
public Test clone(){
Test test = null;
try{
test = (Test)super.clone();
}catch(CloneNotSupportedException e){
}
return test;
}
public void setValue(String value){
this.arrayList.add(value);
}
public ArrayList<String> getValue(){
return this.arrayList;
}
}
public class TestDemo{
public static void main(String[] args){
Test test = new Test();
test.setValue("我");
Test cloneTest = test.clone();
cloneTest.setValue("你");
System.out.println(test.getValue());
}
}输出:
[我,你]
复制代码
按我们的意愿,应该只输出 “我”,克隆对象不应该改变原型对象的值,这里却改变了,为什么呢?
Object 类提供的方法clone只是拷贝对象,其内部的数组、引用对象等都不拷贝,还是指向原型对象的内部元素地址,这种拷贝就叫浅拷贝。非常不安全。但是内部的基本类型,例如int、long、char 都会被拷贝,对于String类型,可以理解为是基本类型,它没有clone方法。
深拷贝
我们对上面的clone
代码修改一下
@Override
public Test clone(){
Test test = null;
try{
test = (Test)super.clone();
test.arrayList = (ArrayList<String>)this.arrayList.clone();
}catch(CloneNotSupportedException e){
}
return test;
}
复制代码
其实就是单独对私有变量拷贝就可以了,这种方式的拷贝,两个对象间就没有任何瓜葛了,这就是深拷贝。
当然,深拷贝还可以通过自己写二进制流的方式来操作,类似下面的操作
public Test deepClone() throws IOException,ClassNotFoundException{
//将对象放入流
ByteArrayOutputStream bao = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bao);
oos.writeObject(this);
//将象从流中取出
ByteArrayInputStream bis = new ByteArrayInputStream(bao.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis);
return (Test)ois.readObject();
}
复制代码
四、使用场景
-
资源优化场景。也就是当进行对象初始化需要使用很多外部资源时,比如,IO 资源、数据文件、CPU、网络和内存等。
-
复杂的依赖场景。 比如,F 对象的创建依赖 A,A 又依赖 B,B 又依赖 C……于是创建过程是一连串对象的 get 和 set。
-
性能和安全要求的场景。 比如,同一个用户在一个会话周期里,可能会反复登录平台或使用某些受限的功能,每一次访问请求都会访问授权服务器进行授权,但如果每次都通过 new 产生一个对象会非常烦琐,这时则可以使用原型模式。
-
同一个对象可能被多个修改者使用的场景。 比如,一个商品对象需要提供给物流、会员、订单等多个服务访问,而且各个调用者可能都需要修改其值时,就可以考虑使用原型模式。
-
需要保存原始对象状态的场景。 比如,记录历史操作的场景中,就可以通过原型模式快速保存记录。
-
结合工厂模式来使用。 在实际项目中,原型模式除了单独基于对象使用外,还可以结合工厂方法模式一起使用,通过定义统一的复制接口,比如 clone、copy。使用一个工厂来统一进行拷贝和新对象创建, 然后由工厂方法提供给调用者使用。
五、优点
-
逃避了构造函数的约束
-
减少每次创建对象的资源消耗
-
降低对象创建的时间消耗
-
快速复制对象运行时状态,复制大对象时,性能更优
-
能保存原始对象的副本
六、缺点
-
原型需要一个被初始化过的正确对象
-
复制大对象时,可能出现内存溢出的 OOM 错误
-
动态扩展对象功能时可能会掩盖新的风险。比如,埋点服务中我们通常会拷贝一份对象在某个时间节点的数据,并添加一些追踪数据后再推送给埋点服务,这样就可能增加过多的内存消耗,影响原有功能执行的性能,有时还可能引起 OOM,导致系统宕机