设计模式(三)——原型模式

一、引子

1、克隆人的问题

  问题:有一个人叫张三,姓名:张三,年龄:18,身高:178。如何创建和张三属性完全相同的10个人呢?
  代码示例:

 1 public class Main {
 2 
 3     public static void main(String[] args) {
 4         Person p0 = new Person("张三", 18, 178);
 5 
 6         Person p1 = new Person(p0.getName(), p0.getAge(), p0.getHeight());
 7         Person p2 = new Person(p0.getName(), p0.getAge(), p0.getHeight());
 8         Person p3 = new Person(p0.getName(), p0.getAge(), p0.getHeight());
 9         Person p4 = new Person(p0.getName(), p0.getAge(), p0.getHeight());
10         Person p5 = new Person(p0.getName(), p0.getAge(), p0.getHeight());
11         Person p6 = new Person(p0.getName(), p0.getAge(), p0.getHeight());
12         Person p7 = new Person(p0.getName(), p0.getAge(), p0.getHeight());
13         Person p8 = new Person(p0.getName(), p0.getAge(), p0.getHeight());
14         Person p9 = new Person(p0.getName(), p0.getAge(), p0.getHeight());
15         Person p10 = new Person(p0.getName(), p0.getAge(), p0.getHeight());
16 
17         System.out.println(JSON.toJSONString(p0));
18     }
19 }
20 
21 class Person {
22     private String name;
23     private int age;
24     private int height;
25     // getter & setter
26     // 有参数、无参构造器
27 }

  这样创建的问题显而易见:如果Person类有100个属性呢?如果要创建和张三属性完全相同的100个人呢?

二、原型模式

1、介绍

  原型模式(Prototype):用原型实例指定创建对象的种类,并且通过拷贝这些原型,创建新的对象,无需知道创建的细节。简单来说就是复制一个和已知对象一模一样的对象。
  方式:实现 Cloneable 接口,复写 Object 类的 clone() 方法。该方法可以将一个 Java 对象复制一份,但调用该方法的类必须实现Cloneable接口,这是一个标志接口,标识该类能够复制且具有复制的能力。如果不实现 Cloneable 接口,直接调用 clone() 方法,会抛出 CloneNotSupportedException 异常。
  源码示例:Object.clone()

1 protected native Object clone() throws CloneNotSupportedException;

  这是一个本地方法,具体实现细节,不需要了解,由操作系统实现。只需要知道它的作用就是复制对象,产生一个新的对象。

2、解决克隆人的问题

  代码示例:实现Cloneable接口,复写clone()方法。

 1 public class Main {
 2 
 3     public static void main(String[] args) {
 4         Person p0 = new Person("张三", 18, 178);
 5 
 6         final Person p1 = p0.clone();
 7 
 8         System.out.println(JSON.toJSONString(p1));
 9         System.out.println(p0 == p1);
10     }
11 }
12 
13 class Person implements Cloneable {
14     private String name;
15     private int age;
16     private int height;
17 
18     @Override
19     public Person clone() {
20         Person person = null;
21         try {
22             person = (Person) super.clone();
23         } catch (CloneNotSupportedException e) {
24             e.printStackTrace();
25         }
26         return person;
27     }
28 
29     // getter & setter
30     // 有参数、无参构造器
31 }
32 
33 // 结果
34 {"age":18,"height":178,"name":"张三"}
35 false

  从结果可以看出,复制了一个和张三一模一样的人。

三、深拷贝与浅拷贝

1、介绍

  在 Java 中有两种数据类型:基本类型和引用类型。
  基本类型,也称为值类型,有八大基本数据类型:byte、short、int、long、float、double、char、boolean。注:String并不是基本数据类型。
  引用类型,有数组、类、接口、枚举等。

  浅拷贝:只复制基本数据类型,以及引用类型的引用。
  深拷贝:复制基本数据类型,以及引用类型的引用,和引用对象的实例。

  如图所示:克隆”张三”。
  浅拷贝,只会克隆张三的基本属性,如姓名,年龄,身高等,不会克隆他的狗,他两共用这只狗。
  深拷贝,除了克隆张三,还会克隆他的狗,他两分别独立拥有各自的狗。

2、浅拷贝

  上述在解决克隆人的问题上,其实就是一种浅拷贝。接下来,假设张三拥有一只狗,叫”旺财”。
  代码示例:

 1 public class Main {
 2 
 3     public static void main(String[] args) {
 4         Dog dog = new Dog("旺财");
 5         Person p0 = new Person("张三", 18, 178, dog);
 6 
 7         final Person p1 = p0.clone();
 8 
 9         // 修改p0 张三的狗的名字为 大黄
10         p0.getDog().setName("大黄");
11 
12         System.out.println(JSON.toJSONString(p1));
13     }
14 }
15 
16 class Person implements Cloneable {
17     private String name;
18     private int age;
19     private int height;
20     private Dog dog; // 引用类属性
21 
22     @Override
23     public Person clone() {
24         Person person = null;
25         try {
26             person = (Person) super.clone();
27         } catch (CloneNotSupportedException e) {
28             e.printStackTrace();
29         }
30         return person;
31     }
32     // getter & setter
33     // 有参数、无参构造器
34 }
35 
36 class Dog {
37     private String name;
38     // getter & setter
39     // 有参数、无参构造器
40 }
41 
42 // 结果
43 {"age":18,"dog":{"name":"大黄"},"height":178,"name":"张三"}

  问题:从结果可以看出,修改了张三的狗的名字,克隆张三的狗的名字也改变了。原因是他两指向的是同一只狗。也就是说对象经过克隆后,只是复制了其引用,其指向的还是同一块堆内存空间,当修改其中一个对象的属性,另一个也会跟着变化。

3、深拷贝

  如果就想要,在克隆张三后,他两各自拥有自己独立的狗,怎么办呢?就要用到深拷贝。
  实现方式一:让引用类属性也具备可拷贝性。
  代码示例:让Dog同样具备可拷贝性

 1 public class Main {
 2 
 3     public static void main(String[] args) {
 4         Dog dog = new Dog("旺财");
 5         Person p0 = new Person("张三", 18, 178, dog);
 6 
 7         final Person p1 = p0.clone();
 8 
 9         // 修改p0 张三的狗的名字为 大黄
10         p0.getDog().setName("大黄");
11 
12         System.out.println(JSON.toJSONString(p1));
13         System.out.println(JSON.toJSONString(p0));
14     }
15 }
16 
17 class Person implements Cloneable {
18     private String name;
19     private int age;
20     private int height;
21     private Dog dog; // 引用类属性
22 
23     @Override
24     public Person clone() {
25         Person person = null;
26         try {
27             // 1.先克隆一个人
28             person = (Person) super.clone();
29 
30             // 2.再克隆一条狗,并让这个狗属于这个人.
31             person.dog = this.dog.clone();
32         } catch (CloneNotSupportedException e) {
33             e.printStackTrace();
34         }
35         return person;
36     }
37     // getter & setter
38     // 有参数、无参构造器
39 }
40 
41 class Dog implements Cloneable {
42     private String name;
43 
44     @Override
45     public Dog clone() {
46         Dog dog = null;
47         try {
48             dog = (Dog) super.clone();
49         } catch (CloneNotSupportedException e) {
50             e.printStackTrace();
51         }
52         return dog;
53     }
54     // getter & setter
55     // 有参数、无参构造器
56 }
57 
58 // 结果
59 {"age":18,"dog":{"name":"旺财"},"height":178,"name":"张三"}
60 {"age":18,"dog":{"name":"大黄"},"height":178,"name":"张三"}

  问题:从结果可以看出,他两各自拥有了独立的狗。这种方式,确实实现了对象的深拷贝。但是有一个问题,如果Dog类还有引用类属性,也需要同样让其具备可拷贝性,嵌套太深的话,很不利于代码的扩展性。
  一般推荐使用方式二来实现。
  实现方式二:序列化。对序列化不了解的,可以先看这篇,对象流。
  代码示例:序列化

 1 public class Main {
 2 
 3     public static void main(String[] args) throws Exception {
 4         Dog dog = new Dog("旺财");
 5         Person p0 = new Person("张三", 18, 178, dog);
 6         // 序列化
 7         ObjectOutputStream output = new ObjectOutputStream(new FileOutputStream("temp.dat"));
 8         output.writeObject(p0);
 9 
10         // 反序列化
11         ObjectInputStream input = new ObjectInputStream(new FileInputStream("temp.dat"));
12         final Person p1 = (Person) input.readObject();
13 
14         // 修改p0 张三的狗的名字为 大黄
15         p0.getDog().setName("大黄");
16 
17         System.out.println(JSON.toJSONString(p1));
18         System.out.println(JSON.toJSONString(p0));
19     }
20 }
21 
22 class Person implements Serializable {
23     private static final long serialVersionUID = 1L;
24     private String name;
25     private int age;
26     private int height;
27     private Dog dog; // 引用类属性
28     // getter & setter
29     // 有参数、无参构造器
30 }
31 
32 class Dog implements Serializable {
33     private static final long serialVersionUID = 1L;
34     private String name;
35     // getter & setter
36     // 有参数、无参构造器
37 }

  注意:因为使用了序列化,引用类属性,要想序列化,也需要具备可序列化。
  代码示例:优化,可抽取为一个工具类

 1 public class CloneUtils {
 2     @SuppressWarnings("unchecked")
 3     public static <T extends Serializable> T deepClone(T obj) {
 4         T cloneObj = null;
 5         try {
 6             // 写入字节流
 7             ByteArrayOutputStream out = new ByteArrayOutputStream();
 8             ObjectOutputStream obs = new ObjectOutputStream(out);
 9 
10             obs.writeObject(obj);
11             obs.close();
12 
13             // 分配内存,写入原始对象,生成新对象
14             ByteArrayInputStream ios = new ByteArrayInputStream(out.toByteArray());
15             ObjectInputStream ois = new ObjectInputStream(ios);
16 
17             cloneObj = (T) ois.readObject();
18             ois.close();
19         } catch (Exception e) {
20             e.printStackTrace();
21         }
22         return cloneObj;
23     }
24 }

4、优点

  性能高:原型模式是在内存二进制流的拷贝,要比直接new一个对象性能好很多,特别是要在一个循环体内产生大量的对象时,原型模式可以很好地体现其优点。
  避免构造函数的约束:这既是它的优点也是缺点,直接在内存中拷贝,构造函数是不会执行的,优点就是减少了约束, 缺点也是减少了约束,需要在实际应用时考虑使用。

四、原型模型在框架中的应用