原型设计模式
原型模式(Prototype Pattern)是用于创建重复的对象,同时又能保证性能。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式之一。
这种模式是实现了一个原型接口,该接口用于创建当前对象的克隆。当直接创建对象的代价比较大时,则采用这种模式。例如,一个对象需要在一个高代价的数据库操作之后被创建。我们可以缓存该对象,在下一个请求时返回它的克隆,在需要的时候更新数据库,以此来减少数据库调用。
从上可知原型设计模式的作用:创建重复对象,调用者不需要知道任何创建的细节,不调用构造函数
原型模式包含如下角色:
- 抽象原型类:规定了具体原型对象必须实现的的 clone() 方法。
- 具体原型类:实现抽象原型类的 clone() 方法,它是可被复制的对象。
- 访问类:使用具体原型类中的 clone() 方法来复制新的对象。
实现Cloneable接口进行创建
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| @Data @AllArgsConstructor @NoArgsConstructor public class Student implements Cloneable { private String name; private String sex; private Integer age; @Override protected Object clone() throws CloneNotSupportedException { return super.clone(); } public static void main(String[] args) throws Exception{ Student stu1 = new Student("张三", "男", 18); Student stu2 = (Student)stu1.clone(); stu2.setName("李四"); System.out.println(stu1); System.out.println(stu2); } }
|
可以看到,把一个学生复制过来,只是改了姓名而已,其他属性完全一样没有改变,需要注意的是,一定要在被拷贝的对象上实现Cloneable接口,否则会抛出CloneNotSupportedException异常。
浅克隆
浅克隆就是如果类里面有地址引用的属性,当我们进行clone时,对于引用类型,他依旧会指向同一个地址,当我们修改时两个类中的引用类型的属性会同时改变。
创建一个新对象,新对象的属性和原来对象完全相同,对于非基本类型属性,仍指向原有属性所指向的对象的内存地址。
如下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| @Data public class Clazz implements Cloneable { private String name; private Student student; @Override protected Object clone() throws CloneNotSupportedException { return super.clone(); } } @Data @AllArgsConstructor @NoArgsConstructor public class Student implements Serializable { private String name; private String sex; private Integer age; } public static void main(String[] args) throws Exception{ Clazz clazz1 = new Clazz(); clazz1.setName("高三一班"); Student stu1 = new Student("张三", "男", 18); clazz1.setStudent(stu1); System.out.println(clazz1); Clazz clazz2 = (Clazz)clazz1.clone(); Student stu2 = clazz2.getStudent(); stu2.setName("李四"); System.out.println(clazz1); System.out.println(clazz2); }
|
从上代码的执行过程中我们可以看出,stu1==stu2这就是浅拷贝的弊端,非基础类型会进行地址的指向,而我们想要的是哪怕是引用类型也是相互独立的互不影响,这就是深拷贝
深拷贝
创建一个新对象,属性中引用的其他对象也会被克隆,不再指向原有对象地址。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| @Data public class Clazz implements Cloneable, Serializable { private String name; private Student student; @Override protected Object clone() throws CloneNotSupportedException { return super.clone(); } protected Object deepClone() throws IOException, ClassNotFoundException { ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(bos); oos.writeObject(this); ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray()); ObjectInputStream ois = new ObjectInputStream(bis); return ois.readObject(); } } public static void main(String[] args) throws Exception{ Clazz clazz1 = new Clazz(); clazz1.setName("高三一班"); Student stu1 = new Student("张三", "男", 18); clazz1.setStudent(stu1); Clazz clazz2 = (Clazz)clazz1.deepClone(); Student stu2 = clazz2.getStudent(); stu2.setName("王五"); System.out.println(clazz1); System.out.println(clazz2); }
|
具体步骤如下:
- 创建一个
ByteArrayOutputStream对象bos,用于存储序列化后的对象数据。 - 创建一个
ObjectOutputStream对象oos,并将bos作为参数传递给它。ObjectOutputStream用于将对象写入到字节流中。 - 调用
oos.writeObject(this)将当前对象(this)写入到字节流中。这样,对象的数据就被序列化了。 - 创建一个
ByteArrayInputStream对象bis,并将bos.toByteArray()作为参数传递给它。ByteArrayInputStream用于从字节数组中读取数据。 - 创建一个
ObjectInputStream对象ois,并将bis作为参数传递给它。ObjectInputStream用于从字节流中读取对象。 - 调用
ois.readObject()从字节流中读取对象,并将其返回。这样就完成了当前对象的深拷贝。
需要注意的是,这个方法使用了序列化和反序列化的方式来实现深拷贝。这意味着被拷贝的对象必须实现Serializable接口,并且其所有成员变量也必须是可序列化的。如果对象的成员变量包含不可序列化的对象,那么需要进行额外的处理才能实现深拷贝。
可以看到,当修改了stu2的姓名时,stu1的姓名并没有被修改了,这说明stu2和stu1已经是不同的对象了,说明Clazz中的Student也被克隆了,不再指向原有对象地址,这就是深克隆。这里需要注意的是,Clazz类和Student类都需要实现Serializable接口,否则会抛出NotSerializableException异常。
总结
适用场景:
- 类初始化消耗资源较多。
- new产生的一个对象需要非常繁琐的过程(数据准备、访问权限等)。
- 构造函数比较复杂。
- 循环体中生产大量对象时。
优点:
- 性能优良,Java自带的原型模式是基于内存二进制流的拷贝,比直接new一个对象性能上提升了许多。
- 可以使用深克隆方式保存对象的状态,使用原型模式将对象复制一份并将其状态保存起来,简化了创建的过程。
缺点:
- 必须配备克隆(或者可拷贝)方法。
- 当对已有类进行改造的时候,需要修改代码,违反了开闭原则。
- 深克隆、浅克隆需要运用得当。