1. 原型模式
1.1 定義
原型模式,用原型實例指定創建對象的種類,並且通過拷貝這些原型創建新的對象。(引自《大話設計模式》)
1.2 使用場景
這裏舉例說明一下。比如說現在我要複製一本書。首先,我定義了這本書的相關屬性,有書名、價格、作者(一本書可以有多個作者)。
package main.mode.yxms;
import java.util.ArrayList;
public class Book {
private String title;//書名
private int price;//價格
private ArrayList<String> authors = new ArrayList<String>();
public void show() {
System.out.println("title:"+title);
String s = "";
for (String author : authors) {
s = s.concat(author);
s = s.concat("、");
}
System.out.println("authors:"+s.substring(0,s.length()-1));
System.out.println("price:"+price);
System.out.println("----------華麗麗的分割線----------");
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public int getPrice() {
return price;
}
public void setPrice(int price) {
this.price = price;
}
public ArrayList<String> getAuthors() {
return authors;
}
public void addAuthors(String author) {
this.authors.add(author);
}
}
這裏注意的是,爲了方便list賦值,我將list的set方法改爲add方法。
好了,我現在要複製書了,複製之前我當然要先創建一本書,然後進行復制,現在我能想到兩種複製方式,一是new,二是=。先說new。
package main.mode.yxms;
public class Test {
public static void main(String[] args) {
//創建一本書
Book book1 = new Book();
book1.setTitle("書名1");
book1.setPrice(10);
book1.addAuthors("作者1");
book1.addAuthors("作者2");
book1.show();
//複製一本書
Book book2 = new Book();
book2.setTitle("書名1");
book2.setPrice(10);
book2.addAuthors("作者1");
book2.addAuthors("作者2");
book2.show();
}
}
結果如下
可以看出,複製的book2和book1一模一樣。
再來說一下第二種方式,=。
package main.mode.yxms;
public class Test {
public static void main(String[] args) {
//創建一本書
Book book1 = new Book();
book1.setTitle("書名1");
book1.setPrice(10);
book1.addAuthors("作者1");
book1.addAuthors("作者2");
book1.show();
//複製一本書
Book book2 = new Book();
book2 = book1;
book2.show();
}
}
結果同上
區別在哪呢,當採用方式二時,我們只是複製了book1的引用,如果我們改變book1,book2也會隨之改變。
package main.mode.yxms;
public class Test {
public static void main(String[] args) {
//創建一本書
Book book1 = new Book();
book1.setTitle("書名1");
book1.setPrice(10);
book1.addAuthors("作者1");
book1.addAuthors("作者2");
book1.show();
//複製一本書
Book book2 = new Book();
book2 = book1;
book2.show();
book1.setPrice(110);
book1.show();
book2.show();
}
}
結果變爲
這裏多說一句,如果是book2修改了內容(如book1.setPrice(110);改爲book2.setPrice(110);結果同上)
現在來說說這兩種方式的缺點:
方式一,比較麻煩,如果book的屬性特別多,我們在複製的時候也要寫很多的set方法,代碼冗餘。好處是book2“複製”的是實際內容,book1改變內容時,book2不受影響。
方式二,代碼簡便,但複製的不是實際內容而是引用。這就相當於book2其實是一本空書,裏面都是白頁,如果想看內容,它告訴要去book1看一樣。
如此看來,我們可以有一個更好的辦法去實現“複製”這項功能。就是本文所說的模型模式。
使用場景:
(1)當源對象屬性較多時。
(2)需要多個拷貝對象,各個拷貝對象需要修改部分值時,可以保障源對象的值不變。
1.3 原型模式類圖
Prototype:原型類,聲明一個克隆自身的接口。在Java中相當於Cloneable接口。
ConcretePrototype:具體的原型類,實現一個克隆自身的方法。
Client:客戶端調用,讓一個原型克隆自身從而創建一個新的對象。
這裏簡單說下Cloneable接口。模型模式的核心就是clone()方法。大家應該知道這是Object的方法,如果打開Object源碼查看,可以看到clone()方法定義,
protected native Object clone() throws CloneNotSupportedException;
Object類是所有類的父類,即每個類都默認繼承了Object類。既然如此,爲什麼這裏還要用Cloneable呢?Cloneable是Java提供的一個接口,用來標示這個對象是可以拷貝的。查看Cloneable的源碼,可以看到這個接口中一個方法都沒有,它只是起到一個標記作用。在jvm中只有這個標記的對象才有可能被拷貝。只是有可能,一個對象要是可以被拷貝,需要重寫clone()方法。即
@Override
public Book clone() {
......
}
這裏強調一點,對象拷貝時,類的構造函數是不會被執行的。
舉個例子,一目瞭然。
public class ConstructorTest implements Cloneable{
public ConstructorTest() {
System.out.println("進來構造函數了~~~");
}
public static void main(String[] args) {
ConstructorTest c = new ConstructorTest();
try {
c.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
}
}
結果
可以看出,構造函數只被執行了一次,應該是new的時候執行的,clone的時候並沒有被執行。
從底層原理來說,Object類的clone方法是從內存中(具體的說就是堆內存)以二進制流的方式進行拷貝,重新分配一個內存塊,那構造函數自然就不會被執行了。(構造函數既然是方法,應該是存在方法區中的。)
2. 實戰應用
2.1 舉個栗子
就上面那個例子,我要複製一本書。可以用Object中的clone()方法進行拷貝。這裏對Book類進行簡單修改,增加clone方法。
package main.mode.yxms;
import java.util.ArrayList;
public class Book implements Cloneable {
private String title;//書名
private ArrayList<String> authors = new ArrayList<String>();//作者
private int price;//價格
/**
* 重寫拷貝方法
*/
@Override
protected Book clone() {
try {
Book book = (Book) super.clone();
return book;
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return null;
}
public void show() {
System.out.println("title:"+title);
String s = "";
for (String author : authors) {
s = s.concat(author);
s = s.concat("、");
}
System.out.println("authors:"+s.substring(0,s.length()-1));
System.out.println("price:"+price);
System.out.println("----------華麗麗的分割線----------");
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public ArrayList<String> getAuthors() {
return authors;
}
public void addAuthors(String author) {
this.authors.add(author);
}
public int getPrice() {
return price;
}
public void setPrice(int price) {
this.price = price;
}
}
客戶端測試
package main.mode.yxms;
public class PrototypeTest {
public static void main(String[] args) {
//創建一本書
Book book1 = new Book();
book1.setTitle("書名1");
book1.addAuthors("作者1");
book1.setPrice(10);
book1.show();
//複印一本書
Book book2 = new Book();
book2 = book1.clone();
book2.show();
//修改book2
book2.setTitle("書名2");
book2.setPrice(12);
book2.addAuthors("作者2");
book2.show();
//此時book1
book1.show();
}
}
結果
從結果可以看出,採用clone方法,book2可以完全複製book1的全部內容。當修改book2的內容時,我們發現,title和price更改不會改變book1的內容,但authors的更改卻改變了book1的內容。這又爲什麼呢?
這裏涉及一個新的問題——淺拷貝與深拷貝。
2.2 淺拷貝
Object類提供的clone方法,只是拷貝對象的基本的數據類型(也包括String),對於對象內部的數組、集合、引用對象等都不拷貝,而是指向原生對象內部元素的地址,這種拷貝就叫做淺拷貝。例如上面的例子。
如上圖所示,book2在拷貝book1時,將title和price的值也拷貝了,但authors只是拷貝了引用地址,指向的內容區域和book1實際上是統一區域,此時如果book1或book2任一方改變了值,另一方也就變了。
這就是淺拷貝。如何改善呢?就是採用深拷貝。
2.3 深拷貝
代碼做如下修改
package main.mode.yxms;
import java.util.ArrayList;
public class Book implements Cloneable {
private String title;//書名
private ArrayList<String> authors = new ArrayList<String>();//作者
private int price;//價格
/**
* 重寫拷貝方法
*/
@Override
protected Book clone() {
try {
Book book = (Book) super.clone();
// 手動對authors對象也調用clone()函數,進行拷貝
book.authors = (ArrayList<String>) this.authors.clone();
return book;
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return null;
}
public void show() {
System.out.println("title:"+title);
String s = "";
for (String author : authors) {
s = s.concat(author);
s = s.concat("、");
}
System.out.println("authors:"+s.substring(0,s.length()-1));
System.out.println("price:"+price);
System.out.println("----------華麗麗的分割線----------");
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public ArrayList<String> getAuthors() {
return authors;
}
public void addAuthors(String author) {
this.authors.add(author);
}
public int getPrice() {
return price;
}
public void setPrice(int price) {
this.price = price;
}
}
客戶端測試(同上面的測試類)
package main.mode.yxms;
public class PrototypeTest {
public static void main(String[] args) {
//創建一本書
Book book1 = new Book();
book1.setTitle("書名1");
book1.addAuthors("作者1");
book1.setPrice(10);
book1.show();
//複印一本書
Book book2 = new Book();
book2 = book1.clone();
book2.show();
//修改book2
book2.setTitle("書名2");
book2.setPrice(12);
book2.addAuthors("作者2");
book2.show();
//此時book1並不受影響
book1.show();
}
}
結果
可以看出,book1不受影響,這就是深拷貝。
3. 總結
優點:
(1)可以保障原生對象的完整性,即初始化信息不發生改變。
(2)隱藏了對象創建的細節。
(3)大多數情況下,提高了性能。
缺點:
(1)當原生對象的屬性較少時,new效率可能會更高。需要考慮實際業務情況使用原型模式。
(2)構造函數不會被拷貝。
最後一句話總結,原型模式其實就是從一個對象再創建另外一個可定製的對象,而且不需要知道任何創建的細節。
說在後面:
本文主要是小貓看《大話設計模式》的筆記式的記錄,方便以後查閱。
當然了,我也閱讀了許多其它博友相關的博文,再加之自己的理解整理出本文,僅供參考。
參考:
《大話設計模式》
設計模式之原型模式 (這個博友寫的很棒,大家可以看看)
23中設計模式之_原型模式(深/淺拷貝) (這個博友寫的也不錯哦,大家參考參考)