原型模式
这种模式是实现了一个原型接口,该接口用于创建当前对象的克隆。当直接创建对象的代价比较大时,则采用这种模式。例如,一个对象需要在一个高代价的数据库操作之后被创建。我们可以缓存该对象,在下一个请求时返回它的克隆,在需要的时候更新数据库,以此来减少数据库调用。
一、定义
原型模式(Prototype Pattern)是用于创建重复的对象,同时又能保证性能。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
原型模式的核心在于拷贝原型对象,以系统中已存在的一个对象为原型,直接基于内存二进制流进行拷贝,无需再经历耗时的对象初始化过程(不调用构造函数),性能提升了许多,当对象的构建过程比较耗时时,可以利用当前系统中已存在的对象作为原型,对基进行克隆(一般是基于二进制流的复制)。
原型模式的重要三大角色:
客户(Client):客户类提出创建对象的请求
抽象原型(Prototype):规定拷贝接口
具体原型(Concrete Prototype):被拷贝的对象。
二、应用场景
大家一定经历过大篇幅的创建Geter,Setter赋值的场景,很多人可能也习惯在赋值进行大量的GIE/SET并没觉得有什么不对,但我觉得这样代码属于纯体力活,就像前面我说反射一样,JSON字符串用反射来做三行代码搞定,这都是一个码农走向架构过程中思想转变的过程,而设计模式就是一个很好的思想借用方式,如果我们在GET/SET场景中用到了原型模式,那可以帮我们避免大量的纯体力劳动,提高开发效率。
三、浅克隆
/** * 抽象原型(Prototype):规定拷贝接口 */ public interface Prototype<T> { T clone(); }
/** * 具体原型(Concrete Prototype):被拷贝的对象。 */ @Data public class ConcretePrototypeA implements Prototype<ConcretePrototypeA>{ private int age; private String name; private List hobbies; public ConcretePrototypeA(int age,String name,List hobbies){ this.age=age; this.name=name; this.hobbies=hobbies; } @Override public ConcretePrototypeA clone() { return new ConcretePrototypeA(this.age,this.name,this.hobbies); } @Override public String toString() { return "ConcretePrototypeA{" + "age=" + age + ", name='" + name + '\'' + ", hobbies=" + hobbies + '}'; } }
/** * 具体原型(Concrete Prototype):被拷贝的对象。 */ @Data public class ConcretePrototypeB implements Prototype<ConcretePrototypeB>{ private int age; private String name; private List hobbies; public ConcretePrototypeB(int age, String name, List hobbies){ this.age=age; this.name=name; this.hobbies=hobbies; } @Override public ConcretePrototypeB clone() { return new ConcretePrototypeB(this.age,this.name,this.hobbies); } @Override public String toString() { return "ConcretePrototypeA{" + "age=" + age + ", name='" + name + '\'' + ", hobbies=" + hobbies + '}'; } }
/** * 客户(Client):客户类提出创建对象的请求 */ public class Client { private Prototype prototype; public Client(Prototype prototype){ this.prototype = prototype; } public Prototype startClone(Prototype prototype){ return (Prototype)prototype.clone(); } }
public class PrototypeTest { public static void main(String[] args) { List hobbies = new ArrayList<String>(); // 创建一个具体的需要克隆的对象 ConcretePrototypeA concretePrototype = new ConcretePrototypeA(18, "prototype", hobbies); // 填充属性,方便测试 System.out.println(concretePrototype); // 创建 Client 对象,准备开始克隆 Client client = new Client(concretePrototype); ConcretePrototypeA concretePrototypeClone = (ConcretePrototypeA) client.startClone(concretePrototype); System.out.println(concretePrototypeClone); System.out.println(concretePrototypeClone==concretePrototype); System.out.println("克隆对象中的引用类型地址值:" + concretePrototypeClone.getHobbies()); System.out.println("原对象中的引用类型地址值:" + concretePrototype.getHobbies()); System.out.println("对象地址比较:"+(concretePrototypeClone.getHobbies() == concretePrototype.getHobbies())); } }
从测试结果看出 hobbies 的引用地址是相同的,意味着复制的不是值,而是引用的地址。这样的话 ,如果我们修改任意一个对象中的属性值,concretePrototype 和concretePrototypeCone 的 hobbies 值都会改变。这就是我们常说的浅克隆。只是完整复制了值类型数据,没有赋值引用对象。换言之,所有的引用对象仍然指向原来的对象
四、分析JDK浅克隆API带来的问题
在java提供的API中,不需要手动创建抽象原型接口,java内置了Cloneable抽象原型接口,自定义的类型只需要实现该接口并重写Object.clone方法即可完成对本类的复制。
通过查看JDK的源码发现,其实Cloneable是一个空接口。java之所以提供Cloneable接口,只是为了在运行时通知java虚拟机可以安全的在该类上使用clone方法。如果没有实现Cloneable接口,调用clone()方法会抛出CloneNotSupportedException异常。
如果使用clone()方法,需要满足以下条件:
对于任何对象o,都有o.clone() != o,也就是说克隆产生的对象和原对象不是同一个对象。
对于任何对象o,都有o.clone().getClass() == o.getClass(),也就是说克隆产生的对象和原对象类型相同。
对于任何对象o,应该有o.clone().equals(o)成立,为什么这样不是绝对的呢,因为如果在不重写hashcode和equals的情况下,是不能保证两个对象equals相等的。
对上面代码改造
/** * 具体原型(Concrete Prototype):被拷贝的对象。 */ @Data public class ConcretePrototypeA implements Cloneable{ private int age; private String name; private List<String> hobbies; @Override public ConcretePrototypeA clone() { try { return (ConcretePrototypeA) super.clone(); } catch (CloneNotSupportedException e) { e.printStackTrace(); } return null; } @Override public String toString() { return "ConcretePrototypeA{" + "age=" + age + ", name='" + name + '\'' + ", hobbies=" + hobbies + '}'; } }
public class PrototypeTest { public static void main(String[] args) { // List hobbies = new ArrayList<String>(); // // // 创建一个具体的需要克隆的对象 // ConcretePrototypeA concretePrototype = new ConcretePrototypeA(18, "prototype", hobbies); // // 填充属性,方便测试 // System.out.println(concretePrototype); // // // 创建 Client 对象,准备开始克隆 // Client client = new Client(concretePrototype); // ConcretePrototypeA concretePrototypeClone = (ConcretePrototypeA) client.startClone(concretePrototype); // System.out.println(concretePrototypeClone); // System.out.println(concretePrototypeClone==concretePrototype); // System.out.println("克隆对象中的引用类型地址值:" + concretePrototypeClone.getHobbies()); // System.out.println("原对象中的引用类型地址值:" + concretePrototype.getHobbies()); // System.out.println("对象地址比较:"+(concretePrototypeClone.getHobbies() == concretePrototype.getHobbies())); ConcretePrototypeA concretePrototype = new ConcretePrototypeA(); concretePrototype.setAge(11); concretePrototype.setName("张三"); List<String> hobbies = new ArrayList<String>(); hobbies.add("没变化"); concretePrototype.setHobbies(hobbies); // 创建一个具体的需要克隆的对象 // 创建 Client 对象,准备开始克隆 ConcretePrototypeA concretePrototypeClone = concretePrototype.clone(); // 填充属性,方便测试 concretePrototypeClone.getHobbies().add("有变化"); System.out.println(concretePrototype); System.out.println(concretePrototypeClone); } }
可以看到,我对克隆之后的值进行修改,基础数据类型改变了,没有影响原对象,但是改变引用类型List,则原对象的值也发生了改变。这就叫浅拷贝,这也是浅克隆最致命的问题。那么怎么解决这个问题呢,使用深克隆可以解决该问题。
五、使用序列化实现深克隆
/** * 深拷贝,增加一个deepClone()方法 */ @Data public class ConcretePrototypeA implements Cloneable, Serializable { private int age; private String name; private List<String> hobbies; @Override public ConcretePrototypeA clone() { try { return (ConcretePrototypeA) super.clone(); } catch (CloneNotSupportedException e) { e.printStackTrace(); } return null; } public ConcretePrototypeA deepClone(){ try { ByteArrayOutputStream bos= new ByteArrayOutputStream(); ObjectOutputStream oos= new ObjectOutputStream(bos); oos.writeObject(this); ByteArrayInputStream bis=new ByteArrayInputStream(bos.toByteArray()); ObjectInputStream ois=new ObjectInputStream(bis); return (ConcretePrototypeA) ois.readObject(); }catch (Exception e){ e.printStackTrace(); return null; } } @Override public String toString() { return "ConcretePrototypeA{" + "age=" + age + ", name='" + name + '\'' + ", hobbies=" + hobbies + '}'; } }
public class ConcretaPrototypeTest { public static void main(String[] args) { ConcretePrototypeA concretePrototypeA=new ConcretePrototypeA(); concretePrototypeA.setAge(11); concretePrototypeA.setName("张三"); List<String> hobbies = new ArrayList<String>(); hobbies.add("没变化"); concretePrototypeA.setHobbies(hobbies); // 创建一个具体的需要克隆的对象 // 创建 Client 对象,准备开始克隆 ConcretePrototypeA concretePrototypeClone = concretePrototypeA.deepClone(); // 填充属性,方便测试 concretePrototypeClone.getHobbies().add("有变化"); System.out.println(concretePrototypeA==concretePrototypeClone); System.out.println(concretePrototypeA); System.out.println(concretePrototypeClone); System.out.println(concretePrototypeA.getHobbies()==concretePrototypeClone.getHobbies()); } }
这次修改克隆对象没有影响到原对象,达到了我们的目的。
六、原型破坏单例
/** * 破坏单例示例 */ public class ConcretePrototype implements Cloneable{ private static ConcretePrototype concretePrototype=new ConcretePrototype(); private ConcretePrototype(){} public static ConcretePrototype getInstance(){ return concretePrototype; } public ConcretePrototype clone(){ try { return (ConcretePrototype) super.clone(); }catch (Exception e){ e.printStackTrace(); } return null; } }
public class ConcretePrototypeTest { public static void main(String[] args) { //创建原型对象 ConcretePrototype concretePrototype=ConcretePrototype.getInstance(); //复制原型对象 ConcretePrototype cloneType=concretePrototype.clone(); System.out.println(concretePrototype==cloneType); } }
看上面结果是把单例破坏了,下面来改动下保证单例
public ConcretePrototype clone(){ return concretePrototype; }
七、Cloneable 源码分析
public Object clone() { try { ArrayList<?> v = (ArrayList<?>) super.clone(); v.elementData = Arrays.copyOf(elementData, size); v.modCount = 0; return v; } catch (CloneNotSupportedException e) { // this shouldn't happen, since we are Cloneable throw new InternalError(e); } }
1、调用父类的克隆方法可以一个ArrayList出来我查看源码没有父类中发现clone方法, 也就是说调用的是Object的clone方法。 那么只是简单的浅拷贝。 既然是浅拷贝说明详见的list和原来的list中的elementData 其实是同一个数组。
2、调用Arrays.copyOf方法复制一份新的数组, 赋值给新的List.这个copyOf方法只是复制数组中的引用, 说明两个数组虽然分开了, 但是内部的存储的对象还是一样的。
克隆的方法克隆了新的List , 但是两个list中装载的对象还是同一份。
验证代码:
public class A implements Serializable{ int x = 0; public static void main(String[] args) throws IOException, ClassNotFoundException { ArrayList<A> list = new ArrayList<>(); list.add(new A()); ArrayList<A> cloneList = (ArrayList<A>) list.clone(); System.out.println("比较克隆前后的对象是否同一个"); System.out.println(list.get(0) == cloneList.get(0)); } }
八、原型模式优点
1、java自带的原型模式基于内存二进制流的复制,在性能上比直接new一个对象更加好
2、可以使用深克隆方式保存对象状态,使用原型模式将对象复制一份,并将其状态保存起来,简化了创建对象的过程,以便在需要的时候使用(例如恢复到历史某一状态),可辅助实现撤销操作。
九、原型模式缺点
1、需要为每一个类都配置一个clone方法
2、clone方法位于类的内部,当对已有类进行改造的时候,需要修改代码,违背了开闭原则。
3、当实现深克隆时,需要编写较为复杂的代码,而且当对象之间存在多重嵌套引用时,为了实现深克隆,每一层对象对应的类都必须支持深克隆,实现起来会比较麻烦,因此,深克隆、㳀克隆需要运用得当。
git源码:https://gitee.com/TongHuaShuShuoWoDeJieJu/design_pattern.git