寫在前面
前段時間通過閱讀《JavaScript高級程序設計》全面學習了一次JavaScript之後,在看到使用原型創建對象時,感覺和Java裏的似曾相識又略有不同,於是重新思考了一次原型模式,便有了這篇文章。需要說明的是本文的重點是原型模式本身的思想,儘管我們會討論Java和JavaScript對原型模式的實現,但真正重要的地方是通過不同語言對原型模式的實現去思考模式本身,而不僅僅是解讀語法。以下是今天要討論的內容:
概念
一句話就可以概括原型模式:以一個對象爲樣板,複製出另一個新的對象。設計模式是跨語言的,所以我們先拋開嚴格的語法定義,即先拋開Java裏的Cloneable接口,JavaScript裏的prototype屬性,僅僅着眼於原型模式本身,這裏有兩個關鍵詞,即“樣板”、“複製”,假設我們有一個Person類如下:
public class Person{
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
}
那麼以下這個簡單的方法便滿足了原型模式的概念。
// 複製一個對象
public Person copyPerson(Person src){
Person person = new Person(src.getName(), src.getAge());
return person;
}
// 調用示例
Person p1 = new Person("李四", 20);
Person p2 = copyPerson(p1);
System.out.println(p2.getName() + "--" + p2.getAge());
p2和p1是兩個不同的實例(主要是指分別佔據不同的內存),但它們歸屬同一個類,而且屬性的內容是一樣的,這就是以p1爲樣板複製一個新對象(p2)。這個例子的寫法非常不符合在Java裏面使用原型模式的規範,但是它足夠簡潔地表達了原型模式的根本思想:複製對象。爲什麼要爲了簡單而先不談規範?因爲設計模式是跨語言的,我們應當先脫去具體語言的外衣,以最簡潔的方式搞懂模式本身,然後纔去關注爲了實現模式而設計出來的語法。
深拷貝和淺拷貝
拷貝即複製,提到複製對象,就不得不提對於引用類型的深拷貝和淺拷貝,爲了加深理解,我們分別列舉拷貝對象和拷貝集合的情況。我們爲Person加一個字段,現在我們有如下兩個對象:
public class Person{
private String name;
private int age;
private Work work; // 工作描述
public Person(String name, int age, Work work) {
this.name = name;
this.age = age;
this.work = work;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
public Work getWork() {
return work;
}
}
public class Work{
private String name; // 公司名稱
private int salary; // 薪水
public Work(String name, int salary) {
this.name = name;
this.salary = salary;
}
public String getName() {
return name;
}
public int getSalary() {
return salary;
}
}
拷貝對象
當我們說對某個對象的屬性進行拷貝時,深/淺拷貝一般是下面的樣子:
// 創建一個待拷貝的對象
Person p = new Person("李四", 20, new Work("abcd有限公司", 10000));
// 淺拷貝
Person p1 = new Person(p.getName(), p.getAge(), p.getWork());
// 深拷貝
Work work = p.getWork();
// 重點在於對Work的拷貝方式有區別
Person p2 = new Person(p.getName(), p.getAge(), new Work(work.getName(), work.getSalary()));
基本數據類型和不可變類型(即String)沒有深淺拷貝一說,或者說只有深拷貝,不存在淺拷貝。然而對於引用類型,即Person的Work屬性,進行淺拷貝時,新舊Person指向的Work是同一個對象,通過新Person獲取並修改Work對象的內容,會影響舊Person。執行深拷貝時,新Person的Work也是一個全新的對象,只是其內容和舊Person的Work一致而已,兩者是相互獨立的。
拷貝集合
當我們說對集合進行拷貝時,深淺拷貝一般是下面的樣子:
// 初始化一個待拷貝的集合
List<Person> srcList = new ArrayList<>();
for(int i = 0; i < 10; i++){
srcList.add(new Person("李四" + i, i, new Work(i + "有限公司", 10000 * i)));
}
// 淺拷貝
List<Person> newList = new ArrayList<>();
for(int i = 0, size = srcList.size(); i < size; i++){
newList.add(srcList.get(i));
}
// 深拷貝
List<Person> newList = new ArrayList<>();
Person person;
Work work;
for(int i = 0, size = srcList.size(); i < size; i++){
person = srcList.get(i);
work = person.getWork();
// 這裏需要注意的是,不僅僅要創建新的Person,每個Person的Work也是新創建的
newList.add(new Person(person.getName(), person.getAge(), new Work(work.getName(), work.getSalary())));
}
對於淺拷貝,兩個集合所指向的都是內存裏的同一套對象,通過其中一個集合獲取並修改對象,會影響到另外一個集合。對於深拷貝,內存裏實際上有兩套相互獨立的對象,通過其中一個集合獲取並修改對象,對另一個集合沒有任何影響。
至此,我們知道了原型模式的關鍵在於提供“樣板”以及對樣板進行“複製”,也瞭解了深淺拷貝,現在我們從抽象的設計模式落實到具體的語法實現。
原型模式在Java裏的實現
Java通過Cloneable接口提供樣板,當一個類實現了Cloneable接口,表示它的對象可以被作爲樣板進行復制。值得一提的是Cloneable是個空接口:
public interface Cloneable {
}
它裏面沒有任何方法,它僅僅表示這個類的對象可以被複制。原型模式的另一個關鍵“複製”是由定義在Object類裏面的clone()方法來表示的,你需要在你的自定義類裏面重寫clone()方法,並調用Object類的clone()方法來進行復制。
爲什麼會搞得那麼奇怪呢?因爲Java提供了真正“複製”對象的方法,不同於通過new來創建新對象,它可以直接在內存層面複製對象,而Java又不允許程序員直接操作內存,於是只能使用Object的clone()來複制對象。實際上,你甚至都看不到JDK裏面clone()的源碼:
protected native Object clone() throws CloneNotSupportedException;
因爲它不是Java的方法,而是native的方法。
這同時解釋了爲什麼clone()沒有定義在Cloneable接口裏面(因爲程序員沒法操作內存,不能實現真正能複製對象的clone()方法),以及爲什麼要在重寫clone()的時候調用Object類的clone()方法(因爲這個方法才能真正在內存層面直接複製對象)。至於爲什麼要實現Cloneable接口,因爲clone()方法會檢查對象是否實現了Cloneable接口,如果沒有實現,會拋出CloneNotSupportedException異常。
最後需要注意的是,Object的clone()方法對引用類型執行的是淺拷貝。
於是使用Cloneable來實現原型模式的寫法如下:
public class Person implements Cloneable{
private String name;
private int age;
private Work work;
public Person(String name, int age, Work work) {
this.name = name;
this.age = age;
this.work = work;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public Work getWork() {
return work;
}
public void setWork(Work work) {
this.work = work;
}
@Override
protected Person clone() {
Person person = null;
try{
person = (Person)super.clone();
} catch (CloneNotSupportedException e){
e.printStackTrace();
}
// 注意對引用類型對象進行深拷貝,這樣才能得到兩個完全獨立的Person,
// 當然如果你就是不想進行深拷貝,也可以不要這行代碼
Work work = person.getWork();
person.setWork(new Work(work.getName(), work.getSalary()));
return person;
}
}
使用:
Person p1 = new Person("李四", 20, new Work("abcd有限公司", 10000));
Person p2 = p1.clone();
小結一下Java是如何實現原型模式的幾個關鍵點的:
- 提供“樣板”:Java通過Cloneable接口表示一個類的對象可以被作爲樣板進行復制。
- 複製樣板:在Object類裏面提供clone()方法實現對象在內存層面的複製。
- 深淺拷貝:Object的clone()方法對引用類型執行的是淺拷貝,程序員在重寫clone()時可以在調用Object的clone()獲取到新對象後“手動地”對引用類型進行深拷貝。
原型模式在JavaScript裏的實現
和Java相比一個很大的區別在於,JavaScript沒有“類”的概念,JavaScript通過函數(爲了區分普通函數,下文用構造函數來稱呼)創建對象,或者說是通過“new + 構造函數”來創建對象。
在Java裏面,可以通過至少兩類方式來創建對象:使用new通過類來創建對象、通過原型模式以clone()的方式創建對象。然而在JavaScript裏面,由於沒有類的概念,並且函數本身也是一個對象,所以通過“new + 構造函數”來創建對象,本身就是以一個對象爲樣板複製出一個新對象。這裏的樣板就是構造函數,而“new + 構造函數名”返回的對象,便是被複製出來的對象。可以說,在JavaScript裏面創建對象“天然”地符合原型模式的思想。
現在我們已經知道了在JavaScript裏面,通過定義構造函數來提供樣板,使用“new + 構造函數”即執行了複製對象的操作,實際上整個複製過程都沒有讓程序員干預,那麼問題來了,深淺拷貝怎麼控制呢?答案是通過系統爲每一個構造函數自動添加的prototype屬性來控制。我們通過一張圖片來看看構造函數、原型、創建出來的對象的關係:
系統會爲你定義的每一個構造函數添加一個prototype屬性,這個屬性指向另外一個系統爲你自動創建的對象,這個對象被稱爲“原型”。你可以通過構造函數來訪問這個對象,爲它添加屬性或方法。在複製對象的時候(即使用“new + 構造函數”創建對象的時候),所有構造函數裏定義的東西(包括屬性和方法)會進行深拷貝 ,所有原型裏的東西(包括屬性和方法)會進行淺拷貝,這裏所謂的淺拷貝,就是每個被創建出來的對象裏面有一個屬性指向它們共同的原型——即構造函數的原型。
小結一下JavaScript是如何實現原型模式的幾個關鍵點的:
- 提供“樣板”:每一個函數都可以被當作一個樣板。
- 複製樣板:使用“new + 構造函數”即可進行復制。
- 深淺拷貝:在構造函數裏定義的東西會被深拷貝,在原型裏定義的東西會被淺拷貝。
總結:重要的是思想,不是語法
交替思考Java和JavaScript對原型模式的實現,其共同點在於它們都實現了原型模式的關鍵:提供樣板和複製對象,也都讓程序員可以自由地配置深淺拷貝,只不過基於各自的語法以不同的方式實現。
其實原型模式非常簡單,就是提供樣板和複製對象(以及深淺拷貝)兩件事,甚至在Java裏面你只要知道Cloneable接口以及如何重寫clone()方法即可。然而學習設計模式重要的不在於語法,而在於理解模式本身的思想和行爲,筆者認爲一個Java程序員去理解JavaScript的原型很有意思,因爲JavaScript沒有類,沒有接口,基於一個完全不同的語法環境去思考如何實現一個你曾經學過的設計模式能夠檢驗你是否真正學會了這個模式,而不是僅僅記住了Cloneable和clone()。