👉本文章全部文字純原創,若是須要轉載,請註明轉載出處,謝謝!😘java
你們好,我是高冷就是範兒,很高興又和你們見面了。😊今天咱們繼續設計模式的探索之路。前幾篇的內容有小夥伴尚未閱讀過的,能夠閱讀一下。git
前文回顧
👉讓設計模式飛一下子|單例模式
👉讓設計模式飛一下子|工廠模式github
今天咱們接下來要聊的是原型模式。編程
❓何爲原型?設計模式
維基百科上給出的概念:原型是獨創的模型,表明同一類型的人物、物件、或觀念。ide
以個人理解能力解釋一下,就是說,它是一種類型的獨創對象。在面向對象編程中,所謂的類型就是指類,也就是說,它是這個類的一個源實例。post
我仍是堅持前面幾篇一向的風格,在深刻了解該模式以前,先來思考一下,這個模式它出現的緣由以及存在的意義是什麼?性能
首先,這個模式也是屬於建立型模式,也是用來建立對象。仍是回到以前反覆說過的一個問題,就是咱們建立對象爲何必定要使用原型模式呢?學習
像以前咱們學過的單例模式是由於須要控制對象個數必須是單個。工廠模式是須要將對象建立和使用解耦,使得能夠在不須要知道建立細節而使用一個對象,那今天要學習的原型模式它用在建立對象上又是出於什麼緣由呢?測試
舉個簡單例子
好比某一個公司有A和B兩個產品線,如今假設須要在每個產品銷售出去以前作一次檢查,檢查標準是,假如該產品的重量超過10kg,就從新生產一個新的。如今但願將全部產品的檢查邏輯用同一個通用方法實現,而且後續增長新產品後能夠方便擴展,怎麼實現這個需求?
你可能會以下實現(僞代碼),
public class ProductCheck {
public void check(Product product) {
if (product.weight > 10) {
//若是該產品是A產品就建立一個新的A對象。
//若是該產品是B產品就建立一個新的對象。
}
}
}
複製代碼
可是如今出問題了,寫不下去發現沒有?爲何?
由於如今檢查的這個對象重量超過10kg了,因此須要建立一個新的對象,但問題這個時候我並不知道傳入的product
對象是什麼類型啊?是A類型?仍是B類型?這個在你編譯時期你是不知道的,天然這代碼你就無法寫下去了......
那該怎麼解決這個問題呢?
有聰明的程序猿說了,這還不簡單嗎?直接在check()
方法中加個if else
判斷一下不就行了嗎?因而代碼優化成以下,
public class ProductCheck {
public void check(Product product) {
if (product.weight > 10) {
if (product instanceof PA) {
//若是該產品是A產品就建立一個新的A對象。
} else if (product instanceof PB) {
//若是該產品是B產品就建立一個新的對象。
}
}
}
}
複製代碼
其中PA
和PB
分別是Product
接口的子類,表示A產品和B產品。上面代碼看上去貌似確實沒啥問題,經過對傳入的product
類型判斷從而建立不一樣類型的對象,很正常嘛?
❓可是這樣寫有個啥問題?
上面需求是這個通用方法須要知足,後續增長新產品後能夠方便擴展。如今假設這個公司新增了一種C產品,也須要使用這個檢查方法怎麼辦?這個時候你就必需要修改check()
方法的代碼,增長else if (product instanceof PC)
的邏輯,還記得開閉原則嗎?這顯然違反了開閉原則,因此這個方案不可取。若是看過上篇工廠模式的同窗可能想起點什麼?這個有點相似工廠模式裏面的簡單工廠模式嘛?
引發這個問題的本質在於哪裏?
沒錯,就是由於check()
跟具體的產品類耦合了。
當時是怎麼解決開閉原則的問題的?
沒錯是經過工廠方法模式解決的,因而優化後代碼以下:
public class ProductCheck {
public void check(ProductFactory factory) {
Product product = factory.createProduct();
if (product.weight > 10) {
Product product2 = factory.createProduct();
}
}
}
複製代碼
其中A產品和B產品各會有ProductFactory
的實現類,這樣當新增產品時就不會出現開閉原則的問題了。沒錯,這個問題用工廠方法模式徹底能夠解決,沒問題。可是今天呢,咱們將要聊的原型模式也可能解決這個問題。這個時候確定會有人問了,既然工廠模式已經能夠解決這一問題,那爲何還要你的原型模式呢?
這個問題我會留到後面講,如今先讓咱們看一下原型模式是怎麼解決這個問題的?
原型模式的原理是這樣的,原型模式要求,每個對象須要定義一個克隆本身的方法。什麼意思?好比一個A對象,他須要提供一個方法,調用這個方法將會返回一個本身的副本。通常來講,會給全部須要克隆本身的對象提供一個公共的接口,這個接口裏面會提供一個克隆自身的方法,以下,
interface CloneableObj {
Object cloneSelf();
}
複製代碼
而後讓全部須要克隆本身的類去實現該接口,天然會須要實現cloneSelf()
方法,這個方法內部就是克隆本身的邏輯實現。那如何實現克隆呢?
最傻瓜的辦法,直接先new一個本身對象的實例,而後把本身實例中的數據取出來,設置到新的對象實例中去,不就能夠完成實例的複製了嘛?這樣這個cloneSelf()
方法返回的就是一個跟自身如出一轍的對象了。如下是代碼實現:
class PA extends Product implements CloneableObj{
@Override
public Object cloneSelf() {
PA a = new PA();
a.weight = weight;
return a;
}
}
複製代碼
這個時候假設你須要在工程其餘代碼中須要經過克隆方式快速獲得一個PA對象,就能夠經過調用原型PA對象(假設是a)a.cloneSelf()
輕鬆快速的獲得一個PA
對象了。
沒錯,這個就是最本質的原型模式。其實說的簡單一點,所謂的原型模式,就是複製(或克隆)模式,就是經過當前對象(原型對象)返回一個跟當前對象徹底相同的對象,包括其中的屬性值。
這也是原型模式跟直接new的一個區別,咱們知道new生成的對象的屬性值都是默認的,而經過原型模式返回的對象是將屬性值一同複製。
其實,原型模式並不強制要求克隆生成的對象和原型對象徹底相同,並且也沒有規定具體採用的克隆技術,這個能夠由程序本身實現。只是在大部分實際應用場景中,用原型模式生成的對象都是和原型對象徹底相同或者相近。
其實,上面這個例子是爲了更好的理解原型模式的本質,爲了提升克隆效率,JDK
已經設計了關於對象克隆的功能。在Object
類中有一個clone()
方法,該方法就能夠輕鬆的實現對對象自己進行克隆。上面例子中底層仍是採用new
的方式建立對象,可是Object.clone()
底層是直接對二進制數據流操做,所以效率會比直接new的方式高得多(看到後面,其實這句話說的不嚴謹)。不過要使用Object.clone()
來對自身對象克隆有個限制,就是該對象所對應的類必需要實現java.lang.Cloneable
接口,不然會拋出CloneNotSupportedException
異常。另外,通常須要被克隆的類都須要重寫Object.clone()
,而且將訪問修飾符改成public
,以方便在其餘類中使用。因而代碼實現以下:
public class ProductCheck {
public void check(Product product) throws CloneNotSupportedException {
if (product.weight > 10) {
Object o = product.clone();
}
}
}
複製代碼
有人又會有疑問了,這樣作相比較前面的工廠模式有啥優點?工廠方法模式也徹底能夠實現相同的需求啊?
原型模式和工廠方法模式一個共同的優勢是,他們均可以在不知道具體的類型狀況之下,建立出某類型對象。好比上面例子中的Product
,這只是一個抽象接口,其下會有不少的子類,具體建立哪一種類型的子類對象取決於運行時期。原型模式是經過克隆自身的方式實現的,而工廠方法模式是經過不一樣子類的工廠類實現的。
可是原型模式相比於工廠方法模式的優點在於,工廠方法模式底層仍是採用new
的方式建立對象,而且須要手動的爲屬性賦值,效率較差。而經過Object.clone()
實現的原型模式直接是操做二進制流實現,並且克隆生成的對象是已經賦好值了。所以效率要高得多。
那麼,經過
new
的方式建立對象和調用clone()
方式建立對象,效率相差多少?
下面給出一個簡單的測試例子:
public class A implements Cloneable {
private String a = "a";
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
public A() {}
}
public class Demo {
public static void main(String[] args) throws CloneNotSupportedException {
A a = new A();
long s1 = System.nanoTime();
for (int i = 0; i < 10000000; i++) {
new A();
}
long e1 = System.nanoTime();
System.out.println("new cost total: " + TimeUnit.NANOSECONDS.toMillis(e1 - s1));
long s2 = System.nanoTime();
for (int i = 0; i < 10000000; i++) {
a.clone();
}
long e2 = System.nanoTime();
System.out.println("clone cost total: " + TimeUnit.NANOSECONDS.toMillis(e2 - s2));
}
}
複製代碼
上面輸出的結果以下:
相信看到這個結果的朋友確定會大吃一驚⁉️震驚!怎麼clone
的速度比new
還慢了這麼多倍......😱和以前的認知截然不同了。
那結果然的是這樣嗎❓
咱們作一下小改動,其它代碼都不作修改,但這一次咱們在new
所須要的構造器中加入一些耗時操做,以下:
public class A implements Cloneable {
//其他代碼和上面的例子同樣,省略,惟一區別在於加入下面的代碼
public A() {
for (int i = 0; i < 1000; i++)
a += "a";
}
}
複製代碼
爲了節省測試時間,咱們把Demo
中的循環次數減小到10000就好,以下:
public class Demo {
public static void main(String[] args) throws CloneNotSupportedException {
A a = new A();
long s1 = System.nanoTime();
for (int i = 0; i < 10000; i++) {
new A();
}
long e1 = System.nanoTime();
System.out.println("new cost total: " + TimeUnit.NANOSECONDS.toMillis(e1 - s1));
long s2 = System.nanoTime();
for (int i = 0; i < 10000; i++) {
a.clone();
}
long e2 = System.nanoTime();
System.out.println("clone cost total: " + TimeUnit.NANOSECONDS.toMillis(e2 - s2));
}
}
複製代碼
這一次測試結果以下:
這一次總算出現符合預期的結果了。
也就是說,對於自己建立過程不是很耗時的簡單對象來講,直接new的效率要比clone要高。可是若是是建立過程很複雜很耗時的對象,那使用clone的方式要比new的方式效率高得多。這也是clone()方法的意義所在。
也就是說,對於建立耗時複雜的對象,用原型模式能夠大大提升建立對象的效率。到這裏估計不少人應該能想到,既然這樣,把這二者結合一下不就能夠彌補工廠方法模式的缺陷了嗎?
沒錯,傳統的工廠方法模式中,各子類的工廠類建立對象的方法,好比上面的factory.createProduct()
底層仍是採用new
的方式,若是改爲克隆方式就能夠大大提升建立對象的效率了。思路比較簡單,具體代碼這邊就不演示了。
另外,在原型模式中還會涉及到一個淺克隆和深克隆的問題,怎麼理解呢?我舉一個簡單的例子,
//如下代碼所有省略setter、getter、toString
public class A {
private int a;
}
public class B implements Cloneable{
private int b;
private A a;
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
public class ShallowClone {
public static void main(String[] args) throws CloneNotSupportedException {
A a = new A();
a.setA(1);
B b = new B();
b.setA(a);
b.setB(2);
B b2 = (B)b.clone(); ❶
System.out.println("b-->" + b);
System.out.println("b2-->" + b2);
b2.setB(3); ❷
System.out.println("b-->" + b);
System.out.println("b2-->" + b2);
b2.getA().setA(10);❸
System.out.println("b-->" + b);
System.out.println("b2-->" + b2);
}
}
//輸出:
b-->B{b=2, a=A{a=1}}
b2-->B{b=2, a=A{a=1}}
b-->B{b=2, a=A{a=1}}
b2-->B{b=3, a=A{a=1}}
b-->B{b=2, a=A{a=10}}
b2-->B{b=3, a=A{a=10}}
複製代碼
從上面這個代碼分析可得出:
b.clone()
克隆獲得的,因此這兩個對象除了內存地址不一樣,其他的內容都相同。int
類型。A
類型(引用類型)。由此,咱們能夠得出一個結論,Object.clone()
實現的實際上是一種淺克隆模式。
在淺克隆模式下,克隆生成對象的基本數據類型(包括對應包裝類)屬性和String拷貝的是值,後續修改克隆對象的該屬性值,並不會影響原來的對象裏的值。但若是是引用類型屬性拷貝的是引用,拷貝獲得的對象和原來的對象的屬性指向同一個對象。因此,後續修改其屬性值,就會影響原來的對象裏的對應的屬性值。
而在有些場合下,咱們是但願原型對象和新建立的對象不要相互干擾。這就是深克隆模式。
❓那怎麼實現呢?
public class A implements Cloneable{
private int a;
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
public class B implements Cloneable{
private Integer b;
private A a;
@Override
public Object clone() throws CloneNotSupportedException {
B b = (B) super.clone(); ❶
A a = b.getA();
b.setA((A) a.clone());
return b;
}
}
public class DeepClone {
public static void main(String[] args) throws CloneNotSupportedException {
A a = new A();
a.setA(1);
B b = new B();
b.setA(a);
b.setB(2);
B b2 = (B)b.clone();
System.out.println("b-->" + b);
System.out.println("b2-->" + b2);
b2.setB(3);
System.out.println("b-->" + b);
System.out.println("b2-->" + b2);
b2.getA().setA(10);
System.out.println("b-->" + b);
System.out.println("b2-->" + b2);
}
}
//輸出:
b-->B{b=2, a=A{a=1}}
b2-->B{b=2, a=A{a=1}}
b-->B{b=2, a=A{a=1}}
b2-->B{b=3, a=A{a=1}}
b-->B{b=2, a=A{a=1}}
b2-->B{b=3, a=A{a=10}}
複製代碼
經過上面代碼執行結果咱們不難看出,這個時候不管是修改b2中的a屬性(引用類型)仍是b屬性(基本類型),都不會影響到原型對象中的值了。
❓那這個是怎麼實現的呢?
深克隆模式實現的關鍵在於❶行處,在B
對象經過調用clone()
複製本身的同時,將a屬性(引用類型)也clone
了一份,而且賦值給生成的b2對象。
深克隆原理就是在每個原型對象執行clone()
方法的時候,同時將該對象中每個引用類型的屬性的內容也拷貝一份,並設置到新建立的對象中。假設,每個引用類型中又嵌套着其它的引用類型的屬性,再重複上面操做,以此類推,遞歸執行下去......這中間只要有一個沒有這樣操做,深克隆就失敗。
這也是原型模式一大缺點,在實現深克隆複製時,每一個原型的子類都必須實現clone()
的操做,尤爲是包含多層嵌套引用類型的對象時,必需要遞歸的讓全部相關對象都正確的實現克隆操做,十分繁瑣易錯。
那有沒有更好的辦法來實現深克隆呢?
固然有!😎
可使用序列化和反序列化的手段實現對象的深克隆!
public class A implements Serializable {
private int a;
}
public class B implements Serializable {
private Integer b;
private A a;
}
public class DeepClone2 {
public static void main(String[] args) throws IOException, ClassNotFoundException {
A a = new A();
a.setA(1);
B b = new B();
b.setA(a);
b.setB(2);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(b);
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()));
B b2 = (B) ois.readObject();
System.out.println("b-->" + b);
System.out.println("b2-->" + b2);
b2.setB(3);
System.out.println("b-->" + b);
System.out.println("b2-->" + b2);
b2.getA().setA(10);
System.out.println("b-->" + b);
System.out.println("b2-->" + b2);
}
}
//輸出:
b-->B{b=2, a=A{a=1}}
b2-->B{b=2, a=A{a=1}}
b-->B{b=2, a=A{a=1}}
b2-->B{b=3, a=A{a=1}}
b-->B{b=2, a=A{a=1}}
b2-->B{b=3, a=A{a=10}}
複製代碼
經過上面的代碼不難看出,序列化和反序列化確實實現了深克隆,並且在實現方式上比以前用重寫clone()
的方式要簡單的多,惟一須要作的就是給須要克隆的對象以及引用類型實現Serializable
接口便可。
最後來作個總結,其實原型模式更適合叫作克隆模式,它的本質就在於經過必定技術手段生成一個自身的副本。這能夠經過咱們在文章最開始那樣手動new一個,也能夠經過Object.clone()
,還能夠經過序列化和反序列化實現。若是原型對象中存在引用類型的屬性,根據是否同時克隆該屬性能夠分爲深克隆模式和淺克隆模式。
在大部分場景下,咱們主要會使用Object.clone()
方法來實現克隆,根據上面對clone()方法執行性能測試結果,在建立大量複雜對象時,這個方法的建立效率要遠高於new的方式。所以若是須要建立大量而且複雜對象時能夠採用原型模式。
另外,原型模式能夠像工廠方法模式同樣,能夠在事先不知道具體類型的前提下建立出對象,也就是基於接口建立對象,並且實現方式比工廠模式更高效簡單。
好了,今天關於原型模式的技術分享就到此結束,下一篇我會繼續分享另外一個設計模式——建造者模式,一塊兒探討設計模式的奧祕。我們不見不散。😊👏