從原型模式到淺拷貝和深拷貝
本文轉(zhuǎn)載自微信公眾號(hào)「JavaKeeper」,作者不假 。轉(zhuǎn)載本文請(qǐng)聯(lián)系JavaKeeper公眾號(hào)。
問題
“如果你有一個(gè)對(duì)象, 并希望生成與其完全相同的一個(gè)復(fù)制品, 你該如何實(shí)現(xiàn)呢?首先, 你必須新建一個(gè)屬于相同類的對(duì)象。然后, 你必須遍歷原始對(duì)象的所有成員變量, 并將成員變量值復(fù)制到新對(duì)象中。
- for (int i = 0; i < 10; i++) {
- Sheep sheep = new Sheep("肖恩"+i+"號(hào)",2+i,"白色");
- System.out.println(sheep.toString());
- }
這種方式是比較容易想到的,但是有幾個(gè)不足
- 在創(chuàng)建新對(duì)象的時(shí)候,總是需要重新獲取原始對(duì)象的屬性,如果創(chuàng)建的對(duì)象比較復(fù)雜,效率會(huì)很低
- 總是需要重新初始化對(duì)象,而不是動(dòng)態(tài)地獲得對(duì)象運(yùn)行時(shí)的狀態(tài), 不夠靈活
- 另一方面,并非所有對(duì)象都能通過(guò)這種方式進(jìn)行復(fù)制, 因?yàn)橛行?duì)象可能擁有私有成員變量, 它們?cè)趯?duì)象本身以外是不可見的
“萬(wàn)物兼對(duì)象的 Java 中的所有類的根類 Object,提供了一個(gè) clone() 方法,該方法可以將一個(gè) Java 對(duì)象復(fù)制一份,但是需要實(shí)現(xiàn) clone() 的類必須要實(shí)現(xiàn)一個(gè)接口 Cloneable,該接口表示該類能夠復(fù)制且具有復(fù)制的能力。這就引出了原型模式。
基本介紹
- 原型模式(Prototype模式)是指:用原型實(shí)例指定創(chuàng)建對(duì)象的種類,并且通過(guò)拷貝這些原型,創(chuàng)建新的對(duì)象
- 原型模式是一種創(chuàng)建型設(shè)計(jì)模式, 使你能夠復(fù)制已有對(duì)象, 而又無(wú)需使代碼依賴它們所屬的類
- 工作原理是:通過(guò)將一個(gè)原型對(duì)象傳給那個(gè)要發(fā)動(dòng)創(chuàng)建的對(duì)象,這個(gè)要發(fā)動(dòng)創(chuàng)建的對(duì)象通過(guò)請(qǐng)求原型對(duì)象拷貝它們自己來(lái)實(shí)施創(chuàng)建,即 對(duì)象**.clone**()
類圖
- Prototype : 原型 (Prototype) 接口將對(duì)克隆方法進(jìn)行聲明
Java 中 Prototype 類需要具備以下兩個(gè)條件
- 實(shí)現(xiàn) Cloneable 接口。在 Java 語(yǔ)言有一個(gè) Cloneable 接口,它的作用只有一個(gè),就是在運(yùn)行時(shí)通知虛擬機(jī)可以安全地在實(shí)現(xiàn)了此接口的類上使用 clone 方法。在 Java 虛擬機(jī)中,只有實(shí)現(xiàn)了這個(gè)接口的類才可以被拷貝,否則在運(yùn)行時(shí)會(huì)拋出 CloneNotSupportedException 異常
- 重寫 Object 類中的 clone 方法。Java 中,所有類的父類都是 Object 類,Object 類中有一個(gè) clone 方法,作用是返回對(duì)象的一個(gè)拷貝
- ConcretePrototype:具體原型 (Concrete Prototype) 類將實(shí)現(xiàn)克隆方法。除了將原始對(duì)象的數(shù)據(jù)復(fù)制到克隆體中之外, 該方法有時(shí)還需處理克隆過(guò)程中的極端情況, 例如克隆關(guān)聯(lián)對(duì)象和梳理遞歸依賴等等。
- Client: 使用原型的客戶端,首先要獲取到原型實(shí)例對(duì)象,然后通過(guò)原型實(shí)例克隆自己,從而創(chuàng)建一個(gè)新的對(duì)象。
實(shí)例
“我們用王二小放羊的例子寫這個(gè)實(shí)例
1、原型類(實(shí)現(xiàn) Clonable)
- @Setter
- @Getter
- @NoArgsConstructor
- @AllArgsConstructor
- class Sheep implements Cloneable {
- private String name;
- private Integer age;
- private String color;
- @Override
- protected Sheep clone() {
- Sheep sheep = null;
- try {
- sheep = (Sheep) super.clone();
- } catch (Exception e) {
- System.out.println(e.getMessage());
- }
- return sheep;
- }
- }
2、具體原型
按業(yè)務(wù)的不同實(shí)現(xiàn)不同的原型對(duì)象,假設(shè)現(xiàn)在主角是王二小,羊群里有山羊、綿羊一大群
- public class Goat extends Sheep{
- public void graze() {
- System.out.println("山羊去吃草");
- }
- }
- public class Lamb extends Sheep{
- public void graze() {
- System.out.println("羔羊去吃草");
- }
- }
3、客戶端
- public class Client {
- static List<Sheep> sheepList = new ArrayList<>();
- public static void main(String[] args) {
- Goat goat = new Goat();
- goat.setName("山羊");
- goat.setAge(3);
- goat.setColor("灰色");
- for (int i = 0; i < 5; i++) {
- sheepList.add(goat.clone());
- }
- Lamb lamb = new Lamb();
- lamb.setName("羔羊");
- lamb.setAge(2);
- lamb.setColor("白色");
- for (int i = 0; i < 5; i++) {
- sheepList.add(lamb.clone());
- System.out.println(lamb.hashCode()+","+lamb.clone().hashCode());
- }
- for (Sheep sheep : sheepList) {
- System.out.println(sheep.toString());
- }
- }
原型模式將克隆過(guò)程委派給被克隆的實(shí)際對(duì)象。模式為所有支持克隆的對(duì)象聲明了一個(gè)通用接口, 該接口讓你能夠克隆對(duì)象,同時(shí)又無(wú)需將代碼和對(duì)象所屬類耦合。通常情況下,這樣的接口中僅包含一個(gè) 克隆方法。
所有的類對(duì) 克隆方法的實(shí)現(xiàn)都非常相似。該方法會(huì)創(chuàng)建一個(gè)當(dāng)前類的對(duì)象, 然后將原始對(duì)象所有的成員變量值復(fù)制到新建的類中。你甚至可以復(fù)制私有成員變量, 因?yàn)榻^大部分編程語(yǔ)言都允許對(duì)象訪問其同類對(duì)象的私有成員變量。
支持克隆的對(duì)象即為原型。當(dāng)你的對(duì)象有幾十個(gè)成員變量和幾百種類型時(shí), 對(duì)其進(jìn)行克隆甚至可以代替子類的構(gòu)造。
優(yōu)勢(shì)
使用原型模式創(chuàng)建對(duì)象比直接 new 一個(gè)對(duì)象在性能上要好的多,因?yàn)?Object 類的 clone 方法是一個(gè)本地方法,它直接操作內(nèi)存中的二進(jìn)制流,特別是復(fù)制大對(duì)象時(shí),性能的差別非常明顯。
使用原型模式的另一個(gè)好處是簡(jiǎn)化對(duì)象的創(chuàng)建,使得創(chuàng)建對(duì)象就像我們?cè)诰庉嬑臋n時(shí)的復(fù)制粘貼一樣簡(jiǎn)單。
因?yàn)橐陨蟽?yōu)點(diǎn),所以在需要重復(fù)地創(chuàng)建相似對(duì)象時(shí)可以考慮使用原型模式。比如需要在一個(gè)循環(huán)體內(nèi)創(chuàng)建對(duì)象,假如對(duì)象創(chuàng)建過(guò)程比較復(fù)雜或者循環(huán)次數(shù)很多的話,使用原型模式不但可以簡(jiǎn)化創(chuàng)建過(guò)程,而且可以使系統(tǒng)的整體性能提高很多。
適用場(chǎng)景
《Head First 設(shè)計(jì)模式》是這么形容原型模式的:當(dāng)創(chuàng)建給定類的實(shí)例的過(guò)程很昂貴或很復(fù)雜時(shí),就是用原型模式。
如果你需要復(fù)制一些對(duì)象,同時(shí)又希望代碼獨(dú)立于這些對(duì)象所屬的具體類,可以使用原型模式。
如果子類的區(qū)別僅在于其對(duì)象的初始化方式, 那么你可以使用該模式來(lái)減少子類的數(shù)量。別人創(chuàng)建這些子類的目的可能是為了創(chuàng)建特定類型的對(duì)象
原型模式在 Spring 中的應(yīng)用
我們都知道 Spring bean 默認(rèn)是單例的,但是有些場(chǎng)景可能需要原型范圍,如下
- <bean id="sheep" class="priv.starfish.prototype.Sheep" scope="prototype">
- <property name="name" value="肖恩"/>
- <property name="age" value="2"/>
- <property name="color" value="白色"/>
- </bean>
同樣,王二小還是有 10 只羊,感興趣的也可以看下他們創(chuàng)建的對(duì)象是不是同一個(gè)
- public class Client {
- public static void main(String[] args) {
- ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
- for (int i = 0; i < 10; i++) {
- Object bean = context.getBean("sheep");
- System.out.println(bean);
- }
- }
- }
感興趣的同學(xué)可以深入源碼看下具體的實(shí)現(xiàn),在 AbstractBeanFactory 的 doGetBean() 方法中
原型模式的注意事項(xiàng)
- 使用原型模式復(fù)制對(duì)象不會(huì)調(diào)用類的構(gòu)造方法。因?yàn)閷?duì)象的復(fù)制是通過(guò)調(diào)用 Object 類的 clone 方法來(lái)完成的,它直接在內(nèi)存中復(fù)制數(shù)據(jù),因此不會(huì)調(diào)用到類的構(gòu)造方法。不但構(gòu)造方法中的代碼不會(huì)執(zhí)行,甚至連訪問權(quán)限都對(duì)原型模式無(wú)效。還記得單例模式嗎?單例模式中,只要將構(gòu)造方法的訪問權(quán)限設(shè)置為 private 型,就可以實(shí)現(xiàn)單例。但是 clone 方法直接無(wú)視構(gòu)造方法的權(quán)限,所以,單例模式與原型模式是沖突的,在使用時(shí)要特別注意。
- 深拷貝與淺拷貝。Object 類的 clone方法只會(huì)拷貝對(duì)象中的基本的數(shù)據(jù)類型,對(duì)于數(shù)組、容器對(duì)象、引用對(duì)象等都不會(huì)拷貝,這就是淺拷貝。如果要實(shí)現(xiàn)深拷貝,必須將原型模式中的數(shù)組、容器對(duì)象、引用對(duì)象等另行拷貝。
淺拷貝和深拷貝
首先需要明白,淺拷貝和深拷貝都是針對(duì)一個(gè)已有對(duì)象的操作。
在 Java 中,除了基本數(shù)據(jù)類型(元類型)之外,還存在 類的實(shí)例對(duì)象 這個(gè)引用數(shù)據(jù)類型。而一般使用 『 = 』號(hào)做賦值操作的時(shí)候。對(duì)于基本數(shù)據(jù)類型,實(shí)際上是拷貝的它的值,但是對(duì)于對(duì)象而言,其實(shí)賦值的只是這個(gè)對(duì)象的引用,將原對(duì)象的引用傳遞過(guò)去,他們實(shí)際上還是指向的同一個(gè)對(duì)象。
而淺拷貝和深拷貝就是在這個(gè)基礎(chǔ)之上做的區(qū)分,如果在拷貝這個(gè)對(duì)象的時(shí)候,只對(duì)基本數(shù)據(jù)類型進(jìn)行了拷貝,而對(duì)引用數(shù)據(jù)類型只是進(jìn)行了引用的傳遞,而沒有真實(shí)的創(chuàng)建一個(gè)新的對(duì)象,則認(rèn)為是淺拷貝。反之,在對(duì)引用數(shù)據(jù)類型進(jìn)行拷貝的時(shí)候,創(chuàng)建了一個(gè)新的對(duì)象,并且復(fù)制其內(nèi)的成員變量,則認(rèn)為是深拷貝。
“所謂的淺拷貝和深拷貝,只是在拷貝對(duì)象的時(shí)候,對(duì) 類的實(shí)例對(duì)象 這種引用數(shù)據(jù)類型的不同操作而已
淺拷貝
- 對(duì)于數(shù)據(jù)類型是基本數(shù)據(jù)類型的成員變量,淺拷貝會(huì)直接進(jìn)行值傳遞,也就是將該屬性值復(fù)制一份給新的對(duì)象。
- 對(duì)于數(shù)據(jù)類型是引用數(shù)據(jù)類型的成員變量,比如說(shuō)成員變量是某個(gè)數(shù)組、某個(gè)類的對(duì)象等,那么淺拷貝會(huì)進(jìn)行引用傳遞,也就是只是將該成員變量的引用值(內(nèi)存地址)復(fù)制一份給新的對(duì)象。因?yàn)閷?shí)際上兩個(gè)對(duì)象的該成員變量都指向同一個(gè)實(shí)例。在這種情況下,在一個(gè)對(duì)象中修改該成員變量會(huì)影響到另一個(gè)對(duì)象的該成員變量值
- 前面我們克隆羊就是淺拷貝,如果我們?cè)?Sheep 中加一個(gè)對(duì)象類型的屬性,public Sheep child;可以看到 s 和 s1 的 friend 是同一個(gè)。
- Sheep s = new Sheep();
- s.setName("sss");
- s.friend = new Sheep();
- s.friend.setName("喜洋洋");
- Sheep s1 = s.clone();
- System.out.println(s == s1);
- System.out.println(s.hashCode()+"---"+s.clone().hashCode());
- System.out.println(s.friend == s1.friend);
- System.out.println(s.friend.hashCode() + "---" +s1.friend.hashCode());
- false
- 621009875---1265094477
- true
- 2125039532---2125039532
深拷貝
現(xiàn)在我們知道 clone() 方法,只能對(duì)當(dāng)前對(duì)象進(jìn)行淺拷貝,引用類型依然是在傳遞引用。那如何進(jìn)行一個(gè)深拷貝呢?
常見的深拷貝實(shí)現(xiàn)方式有兩種:
- 重寫 clone 方法來(lái)實(shí)現(xiàn)深拷貝
- 通過(guò)對(duì)象序列化實(shí)現(xiàn)深拷貝
淺拷貝和深拷貝只是相對(duì)的,如果一個(gè)對(duì)象內(nèi)部只有基本數(shù)據(jù)類型,那用 clone() 方法獲取到的就是這個(gè)對(duì)象的深拷貝,而如果其內(nèi)部還有引用數(shù)據(jù)類型,那用 clone() 方法就是一次淺拷貝的操作。