Java中的對象拷貝(Object Copy)指的是将一個對象的所有屬性(成員變量)拷貝到另一個有着相同類類型的對象中去。舉例說明:比如,對象A和對象B都屬于類S,具有屬性a和b。那麼對對象A進行拷貝操作指派給對象B就是:B.a=A.a; B.b=A.b;
在程式中拷貝對象是很常見的,主要是為了在新的上下文環境中複用現有對象的部分或全部 資料。
Java中的對象拷貝主要分為:淺拷貝(Shallow Copy)、深拷貝(Deep Copy)。
先介紹一點鋪墊知識:Java中的資料類型分為基本資料類型和引用資料類型。對于這兩種資料類型,在進行指派操作、用作方法參數或傳回值時,會有值傳遞和引用(位址)傳遞的差别。
淺拷貝(Shallow Copy):①對于資料類型是基本資料類型的成員變量,淺拷貝會直接進行值傳遞,也就是将該屬性值複制一份給新的對象。因為是兩份不同的資料,是以對其中一個對象的該成員變量值進行修改,不會影響另一個對象拷貝得到的資料。②對于資料類型是引用資料類型的成員變量,比如說成員變量是某個數組、某個類的對象等,那麼淺拷貝會進行引用傳遞,也就是隻是将該成員變量的引用值(記憶體位址)複制一份給新的對象。因為實際上兩個對象的該成員變量都指向同一個執行個體。在這種情況下,在一個對象中修改該成員變量會影響到另一個對象的該成員變量值。
具體模型如圖所示:可以看到基本資料類型的成員變量,對其值建立了新的拷貝。而引用資料類型的成員變量的執行個體仍然是隻有一份,兩個對象的該成員變量都指向同一個執行個體。
淺拷貝的實作方式主要有三種:
一、通過拷貝構造方法實作淺拷貝:
拷貝構造方法指的是該類的構造方法參數為該類的對象。使用拷貝構造方法可以很好地完成淺拷貝,直接通過一個現有的對象建立出與該對象屬性相同的新的對象。
代碼參考如下:
運作結果為:
p1是搖頭耶稣 20
p2是搖頭耶稣 20
修改後的p1是小傻瓜 99
修改後的p2是搖頭耶稣 99
結果分析:這裡對Person類選擇了兩個具有代表性的屬性值:一個是引用傳遞類型;另一個是字元串類型(屬于常量)。
通過拷貝構造方法進行了淺拷貝,各屬性值成功複制。其中,p1值傳遞部分的屬性值發生變化時,p2不會随之改變;而引用傳遞部分屬性值發生變化時,p2也随之改變。
要注意:如果在拷貝構造方法中,對引用資料類型變量逐一開辟新的記憶體空間,建立新的對象,也可以實作深拷貝。而對于一般的拷貝構造,則一定是淺拷貝。
二、通過重寫clone()方法進行淺拷貝:
Object類是類結構的根類,其中有一個方法為protected Object clone() throws CloneNotSupportedException,這個方法就是進行的淺拷貝。有了這個淺拷貝模闆,我們可以通過調用clone()方法來實作對象的淺拷貝。但是需要注意:1、Object類雖然有這個方法,但是這個方法是受保護的(被protected修飾),是以我們無法直接使用。2、使用clone方法的類必須實作Cloneable接口,否則會抛出異常CloneNotSupportedException。對于這兩點,我們的解決方法是,在要使用clone方法的類中重寫clone()方法,通過super.clone()調用Object類中的原clone方法。
參考代碼如下:對Student類的對象進行拷貝,直接重寫clone()方法,通過調用clone方法即可完成淺拷貝。
運作結果如下:
姓名是: 搖頭耶稣, 年齡為: 20, 長度是: 175
姓名是: 大傻子, 年齡為: 99, 長度是: 216
姓名是: 搖頭耶稣, 年齡為: 99, 長度是: 175
其中:Student類的成員變量我有代表性地設定了三種:基本資料類型的成員變量length,引用資料類型的成員變量aage和字元串String類型的name.
分析結果可以驗證:
基本資料類型是值傳遞,是以修改值後不會影響另一個對象的該屬性值;
引用資料類型是位址傳遞(引用傳遞),是以修改值後另一個對象的該屬性值會同步被修改。
String類型非常特殊,是以我額外設定了一個字元串類型的成員變量來進行說明。首先,String類型屬于引用資料類型,不屬于基本資料類型,但是String類型的資料是存放在常量池中的,也就是無法修改的!也就是說,當我将name屬性從“搖頭耶稣”改為“大傻子"後,并不是修改了這個資料的值,而是把這個資料的引用從指向”搖頭耶稣“這個常量改為了指向”大傻子“這個常量。在這種情況下,另一個對象的name屬性值仍然指向”搖頭耶稣“不會受到影響。
深拷貝:首先介紹對象圖的概念。設想一下,一個類有一個對象,其成員變量中又有一個對象,該對象指向另一個對象,另一個對象又指向另一個對象,直到一個确定的執行個體。這就形成了對象圖。那麼,對于深拷貝來說,不僅要複制對象的所有基本資料類型的成員變量值,還要為所有引用資料類型的成員變量申請存儲空間,并複制每個引用資料類型成員變量所引用的對象,直到該對象可達的所有對象。也就是說,對象進行深拷貝要對整個對象圖進行拷貝!
簡單地說,深拷貝對引用資料類型的成員變量的對象圖中所有的對象都開辟了記憶體空間;而淺拷貝隻是傳遞位址指向,新的對象并沒有對引用資料類型建立記憶體空間。
深拷貝模型如圖所示:可以看到所有的成員變量都進行了複制。
因為建立記憶體空間和拷貝整個對象圖,是以深拷貝相比于淺拷貝速度較慢并且花銷較大。
深拷貝的實作方法主要有兩種:
一、通過重寫clone方法來實作深拷貝
與通過重寫clone方法實作淺拷貝的基本思路一樣,隻需要為對象圖的每一層的每一個對象都實作Cloneable接口并重寫clone方法,最後在最頂層的類的重寫的clone方法中調用所有的clone方法即可實作深拷貝。簡單的說就是:每一層的每個對象都進行淺拷貝=深拷貝。
參考代碼如下:
分析結果可以驗證:進行了深拷貝之後,無論是什麼類型的屬性值的修改,都不會影響另一個對象的屬性值。
二、通過對象序列化實作深拷貝
雖然層次調用clone方法可以實作深拷貝,但是顯然代碼量實在太大。特别對于屬性數量比較多、層次比較深的類而言,每個類都要重寫clone方法太過繁瑣。
将對象序列化為位元組序列後,預設會将該對象的整個對象圖進行序列化,再通過反序列即可完美地實作深拷貝。
可以通過很簡潔的代碼即可完美實作深拷貝。不過要注意的是,如果某個屬性被transient修飾,那麼該屬性就無法被拷貝了。
三、json序列化方式
以上是淺拷貝的深拷貝的差別和實作方式。
### 淺拷貝的補充
Spring的beanutils的copypropertires是淺拷貝的實作方式。