原型设计模式

原型模式(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);// Student(name=张三, sex=男, age=18)
System.out.println(stu2);// Student(name=李四, sex=男, age=18)
}
}

可以看到,把一个学生复制过来,只是改了姓名而已,其他属性完全一样没有改变,需要注意的是,一定要在被拷贝的对象上实现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(name=高三一班, student=Student(name=张三, sex=男, age=18))
// 进行克隆
Clazz clazz2 = (Clazz)clazz1.clone();
Student stu2 = clazz2.getStudent();
stu2.setName("李四");// 这里修改的是stu2
System.out.println(clazz1); // Clazz(name=高三一班, student=Student(name=李四, sex=男, age=18))
System.out.println(clazz2); // Clazz(name=高三一班, student=Student(name=李四, sex=男, age=18))
}

从上代码的执行过程中我们可以看出,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); // Clazz(name=高三一班, student=Student(name=张三, sex=男, age=18))
System.out.println(clazz2); // Clazz(name=高三一班, student=Student(name=王五, sex=男, age=18))
}

具体步骤如下:

  1. 创建一个ByteArrayOutputStream对象bos,用于存储序列化后的对象数据。
  2. 创建一个ObjectOutputStream对象oos,并将bos作为参数传递给它。ObjectOutputStream用于将对象写入到字节流中。
  3. 调用oos.writeObject(this)将当前对象(this)写入到字节流中。这样,对象的数据就被序列化了。
  4. 创建一个ByteArrayInputStream对象bis,并将bos.toByteArray()作为参数传递给它。ByteArrayInputStream用于从字节数组中读取数据。
  5. 创建一个ObjectInputStream对象ois,并将bis作为参数传递给它。ObjectInputStream用于从字节流中读取对象。
  6. 调用ois.readObject()从字节流中读取对象,并将其返回。这样就完成了当前对象的深拷贝。

需要注意的是,这个方法使用了序列化和反序列化的方式来实现深拷贝。这意味着被拷贝的对象必须实现Serializable接口,并且其所有成员变量也必须是可序列化的。如果对象的成员变量包含不可序列化的对象,那么需要进行额外的处理才能实现深拷贝。

可以看到,当修改了stu2的姓名时,stu1的姓名并没有被修改了,这说明stu2stu1已经是不同的对象了,说明Clazz中的Student也被克隆了,不再指向原有对象地址,这就是深克隆。这里需要注意的是,Clazz类和Student类都需要实现Serializable接口,否则会抛出NotSerializableException异常。

总结

适用场景:

  • 类初始化消耗资源较多。
  • new产生的一个对象需要非常繁琐的过程(数据准备、访问权限等)。
  • 构造函数比较复杂。
  • 循环体中生产大量对象时。

优点:

  • 性能优良,Java自带的原型模式是基于内存二进制流的拷贝,比直接new一个对象性能上提升了许多。
  • 可以使用深克隆方式保存对象的状态,使用原型模式将对象复制一份并将其状态保存起来,简化了创建的过程。

缺点:

  • 必须配备克隆(或者可拷贝)方法。
  • 当对已有类进行改造的时候,需要修改代码,违反了开闭原则。
  • 深克隆、浅克隆需要运用得当。