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