設計模式(三)——原型模式

L發表於2022-01-07

一、引子

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一個物件效能好很多,特別是要在一個迴圈體內產生大量的物件時,原型模式可以很好地體現其優點。
  避免建構函式的約束:這既是它的優點也是缺點,直接在記憶體中拷貝,建構函式是不會執行的,優點就是減少了約束, 缺點也是減少了約束,需要在實際應用時考慮使用。

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

 

相關文章