Java設計模式之原型模式(Prototype)—— 淺拷貝和深拷貝

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中設計模式之_原型模式(深/淺拷貝)  (這個博友寫的也不錯哦,大家參考參考)

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章