Java的深拷貝和淺拷貝深入了解
關(guān)于Java的深拷貝和淺拷貝,簡(jiǎn)單來(lái)說(shuō)就是創(chuàng)建一個(gè)和已知對(duì)象一模一樣的對(duì)象。可能日常編碼過(guò)程中用的不多,但是這是一個(gè)面試經(jīng)常會(huì)問(wèn)的問(wèn)題,而且了解深拷貝和淺拷貝的原理,對(duì)于Java中的所謂值傳遞或者引用傳遞將會(huì)有更深的理解。
1、創(chuàng)建對(duì)象的5種方式
①、通過(guò) new 關(guān)鍵字
這是最常用的一種方式,通過(guò) new 關(guān)鍵字調(diào)用類(lèi)的有參或無(wú)參構(gòu)造方法來(lái)創(chuàng)建對(duì)象。比如 Object obj = new Object();
②、通過(guò) Class 類(lèi)的 newInstance() 方法
這種默認(rèn)是調(diào)用類(lèi)的無(wú)參構(gòu)造方法創(chuàng)建對(duì)象。比如Person p2 = (Person) Class.forName("com.ys.test.Person").newInstance();
③、通過(guò)Constructor 類(lèi)的 newInstance 方法
這和第二種方法類(lèi)時(shí),都是通過(guò)反射來(lái)實(shí)現(xiàn)。通過(guò)java.lang.relect.Constructor
類(lèi)的newInstance() 方
法指定某個(gè)構(gòu)造器來(lái)創(chuàng)建對(duì)象。
Person p3 = (Person) Person.class.getConstructors()[0].newInstance();
實(shí)際上第二種方法利用 Class 的 newInstance()
方法創(chuàng)建對(duì)象,其內(nèi)部調(diào)用還是 Constructor 的 newInstance() 方法。
④、利用 Clone 方法
Clone 是 Object 類(lèi)中的一個(gè)方法,通過(guò) 對(duì)象A.clone() 方法會(huì)創(chuàng)建一個(gè)內(nèi)容和對(duì)象 A 一模一樣的對(duì)象 B,clone 克隆,顧名思義就是創(chuàng)建一個(gè)一模一樣的對(duì)象出來(lái)。
Person p4 = (Person) p3.clone();
⑤、反序列化
序列化是把堆內(nèi)存中的 Java 對(duì)象數(shù)據(jù),通過(guò)某種方式把對(duì)象存儲(chǔ)到磁盤(pán)文件中或者傳遞給其他網(wǎng)絡(luò)節(jié)點(diǎn)(在網(wǎng)絡(luò)上傳輸)。而反序列化則是把磁盤(pán)文件中的對(duì)象數(shù)據(jù)或者把網(wǎng)絡(luò)節(jié)點(diǎn)上的對(duì)象數(shù)據(jù),恢復(fù)成Java對(duì)象模型的過(guò)程。
2、Clone 方法
本篇博客我們講解的是 Java 的深拷貝和淺拷貝,其實(shí)現(xiàn)方式正是通過(guò)調(diào)用 Object 類(lèi)的 clone() 方法來(lái)完成。在 Object.class 類(lèi)中,源碼為:
protected native Object clone() throws CloneNotSupportedException;
這是一個(gè)用 native 關(guān)鍵字修飾的方法,關(guān)于native關(guān)鍵字有一篇博客專門(mén)有介紹,不理解也沒(méi)關(guān)系,只需要知道用 native 修飾的方法就是告訴操作系統(tǒng),這個(gè)方法我不實(shí)現(xiàn)了,讓操作系統(tǒng)去實(shí)現(xiàn)。具體怎么實(shí)現(xiàn)我們不需要了解,只需要知道 clone方法的作用就是復(fù)制對(duì)象,產(chǎn)生一個(gè)新的對(duì)象。那么這個(gè)新的對(duì)象和原對(duì)象是什么關(guān)系呢?
3、基本類(lèi)型和引用類(lèi)型
這里再給大家普及一個(gè)概念,在 Java 中基本類(lèi)型和引用類(lèi)型的區(qū)別。
在 Java 中數(shù)據(jù)類(lèi)型可以分為兩大類(lèi):基本類(lèi)型和引用類(lèi)型。
基本類(lèi)型也稱為值類(lèi)型,分別是字符類(lèi)型 char,布爾類(lèi)型 boolean以及數(shù)值類(lèi)型 byte
、short
、int
、long
、float
、double
。
引用類(lèi)型則包括類(lèi)、接口、數(shù)組、枚舉等。
Java 將內(nèi)存空間分為堆和棧?;绢?lèi)型直接在棧中存儲(chǔ)數(shù)值,而引用類(lèi)型是將引用放在棧中,實(shí)際存儲(chǔ)的值是放在堆中,通過(guò)棧中的引用指向堆中存放的數(shù)據(jù)?! ?/p>
上圖定義的 a 和 b 都是基本類(lèi)型,其值是直接存放在棧中的;而 c 和 d 是 String 聲明的,這是一個(gè)引用類(lèi)型,引用地址是存放在 棧中,然后指向堆的內(nèi)存空間。
下面 d = c;這條語(yǔ)句表示將 c 的引用賦值給 d,那么 c 和 d 將指向同一塊堆內(nèi)存空間。
4、淺拷貝
我們看如下這段代碼:
package com.ys.test; public class Person implements Cloneable{ public String pname; public int page; public Address address; public Person() {} public Person(String pname,int page){ this.pname = pname; this.page = page; this.address = new Address(); } @Override protected Object clone() throws CloneNotSupportedException { return super.clone(); } public void setAddress(String provices,String city ){ address.setAddress(provices, city); } public void display(String name){ System.out.println(name+":"+"pname=" + pname + ", page=" + page +","+ address); } public String getPname() { return pname; } public void setPname(String pname) { this.pname = pname; } public int getPage() { return page; } public void setPage(int page) { this.page = page; } }
package com.ys.test; public class Address { private String provices; private String city; public void setAddress(String provices,String city){ this.provices = provices; this.city = city; } @Override public String toString() { return "Address [provices=" + provices + ", city=" + city + "]"; } }
這是一個(gè)我們要進(jìn)行賦值的原始類(lèi) Person。下面我們產(chǎn)生一個(gè) Person 對(duì)象,并調(diào)用其 clone 方法復(fù)制一個(gè)新的對(duì)象。
注意:調(diào)用對(duì)象的 clone 方法,必須要讓類(lèi)實(shí)現(xiàn)Cloneable 接口,并且覆寫(xiě) clone 方法。
測(cè)試:
@Test public void testShallowClone() throws Exception{ Person p1 = new Person("zhangsan",21); p1.setAddress("湖北省", "武漢市"); Person p2 = (Person) p1.clone(); System.out.println("p1:"+p1); System.out.println("p1.getPname:"+p1.getPname().hashCode()); System.out.println("p2:"+p2); System.out.println("p2.getPname:"+p2.getPname().hashCode()); p1.display("p1"); p2.display("p2"); p2.setAddress("湖北省", "荊州市"); System.out.println("將復(fù)制之后的對(duì)象地址修改:"); p1.display("p1"); p2.display("p2"); }
打印結(jié)果為:
首先看原始類(lèi) Person 實(shí)現(xiàn)Cloneable 接口,并且覆寫(xiě) clone 方法,它還有三個(gè)屬性,一個(gè)引用類(lèi)型 String定義的 pname,一個(gè)基本類(lèi)型 int定義的 page,還有一個(gè)引用類(lèi)型 Address ,這是一個(gè)自定義類(lèi),這個(gè)類(lèi)也包含兩個(gè)屬性 pprovices 和 city 。
接著看測(cè)試內(nèi)容,首先我們創(chuàng)建一個(gè)Person 類(lèi)的對(duì)象 p1,其pname 為zhangsan,page為21,地址類(lèi) Address 兩個(gè)屬性為 湖北省和武漢市。接著我們調(diào)用 clone() 方法復(fù)制另一個(gè)對(duì)象 p2,接著打印這兩個(gè)對(duì)象的內(nèi)容。
從第 1 行和第 3 行打印結(jié)果:
p1:com.ys.test.Person@349319f9
p2:com.ys.test.Person@258e4566
可以看出這是兩個(gè)不同的對(duì)象。
從第 5 行和第 6 行打印的對(duì)象內(nèi)容看,原對(duì)象 p1 和克隆出來(lái)的對(duì)象 p2 內(nèi)容完全相同。
代碼中我們只是更改了克隆對(duì)象 p2 的屬性 Address 為湖北省荊州市(原對(duì)象 p1 是湖北省武漢市) ,但是從第 7 行和第 8 行打印結(jié)果來(lái)看,原對(duì)象 p1 和克隆對(duì)象 p2 的 Address 屬性都被修改了。
也就是說(shuō)對(duì)象 Person 的屬性 Address,經(jīng)過(guò) clone 之后,其實(shí)只是復(fù)制了其引用,他們指向的還是同一塊堆內(nèi)存空間,當(dāng)修改其中一個(gè)對(duì)象的屬性 Address,另一個(gè)也會(huì)跟著變化?! ?/p>
淺拷貝:創(chuàng)建一個(gè)新對(duì)象,然后將當(dāng)前對(duì)象的非靜態(tài)字段復(fù)制到該新對(duì)象,如果字段是值類(lèi)型的,那么對(duì)該字段執(zhí)行復(fù)制;如果該字段是引用類(lèi)型的話,則復(fù)制引用但不復(fù)制引用的對(duì)象。因此,原始對(duì)象及其副本引用同一個(gè)對(duì)象。
5、深拷貝
弄清楚了淺拷貝,那么深拷貝就很容易理解了。
深拷貝:創(chuàng)建一個(gè)新對(duì)象,然后將當(dāng)前對(duì)象的非靜態(tài)字段復(fù)制到該新對(duì)象,無(wú)論該字段是值類(lèi)型的還是引用類(lèi)型,都復(fù)制獨(dú)立的一份。當(dāng)你修改其中一個(gè)對(duì)象的任何內(nèi)容時(shí),都不會(huì)影響另一個(gè)對(duì)象的內(nèi)容。
那么該如何實(shí)現(xiàn)深拷貝呢?Object 類(lèi)提供的 clone 是只能實(shí)現(xiàn) 淺拷貝的。
6、如何實(shí)現(xiàn)深拷貝?
深拷貝的原理我們知道了,就是要讓原始對(duì)象和克隆之后的對(duì)象所具有的引用類(lèi)型屬性不是指向同一塊堆內(nèi)存,這里有三種實(shí)現(xiàn)思路。
①、讓每個(gè)引用類(lèi)型屬性內(nèi)部都重寫(xiě)clone() 方法
既然引用類(lèi)型不能實(shí)現(xiàn)深拷貝,那么我們將每個(gè)引用類(lèi)型都拆分為基本類(lèi)型,分別進(jìn)行淺拷貝。比如上面的例子,Person 類(lèi)有一個(gè)引用類(lèi)型 Address(其實(shí)String 也是引用類(lèi)型,但是String類(lèi)型有點(diǎn)特殊,后面會(huì)詳細(xì)講解),我們?cè)?Address 類(lèi)內(nèi)部也重寫(xiě) clone 方法。如下:
Address.class:
package com.ys.test; public class Address implements Cloneable{ private String provices; private String city; public void setAddress(String provices,String city){ this.provices = provices; this.city = city; } @Override public String toString() { return "Address [provices=" + provices + ", city=" + city + "]"; } @Override protected Object clone() throws CloneNotSupportedException { return super.clone(); } }
Person.class 的 clone() 方法:
@Override protected Object clone() throws CloneNotSupportedException { Person p = (Person) super.clone(); p.address = (Address) address.clone(); return p; }
測(cè)試還是和上面一樣,我們會(huì)發(fā)現(xiàn)更改了p2對(duì)象的Address屬性,p1 對(duì)象的 Address 屬性并沒(méi)有變化。
但是這種做法有個(gè)弊端,這里我們Person 類(lèi)只有一個(gè) Address 引用類(lèi)型,而 Address 類(lèi)沒(méi)有,所以我們只用重寫(xiě) Address 類(lèi)的clone 方法,但是如果 Address 類(lèi)也存在一個(gè)引用類(lèi)型,那么我們也要重寫(xiě)其clone 方法,這樣下去,有多少個(gè)引用類(lèi)型,我們就要重寫(xiě)多少次,如果存在很多引用類(lèi)型,那么代碼量顯然會(huì)很大,所以這種方法不太合適。
②、利用序列化
序列化是將對(duì)象寫(xiě)到流中便于傳輸,而反序列化則是把對(duì)象從流中讀取出來(lái)。這里寫(xiě)到流中的對(duì)象則是原始對(duì)象的一個(gè)拷貝,因?yàn)樵紝?duì)象還存在 JVM 中,所以我們可以利用對(duì)象的序列化產(chǎn)生克隆對(duì)象,然后通過(guò)反序列化獲取這個(gè)對(duì)象。
注意每個(gè)需要序列化的類(lèi)都要實(shí)現(xiàn)Serializable 接口,如果有某個(gè)屬性不需要序列化,可以將其聲明為transient,即將其排除在克隆屬性之外。
//深度拷貝 public Object deepClone() throws Exception{ // 序列化 ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(bos); oos.writeObject(this); // 反序列化 ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray()); ObjectInputStream ois = new ObjectInputStream(bis); return ois.readObject(); }
因?yàn)樾蛄谢a(chǎn)生的是兩個(gè)完全獨(dú)立的對(duì)象,所有無(wú)論嵌套多少個(gè)引用類(lèi)型,序列化都是能實(shí)現(xiàn)深拷貝的。
總結(jié)
本篇文章就到這里了,希望能夠給你帶來(lái)幫助,也希望您能夠多多關(guān)注腳本之家的更多內(nèi)容!
相關(guān)文章
Java?spring?boot發(fā)送郵箱實(shí)現(xiàn)過(guò)程記錄
我們?cè)?站上注冊(cè)賬號(hào)的時(shí)候?般需要獲取驗(yàn)證碼,?這個(gè)驗(yàn)證碼?般發(fā)送在你的?機(jī)號(hào)上還有的是發(fā)送在你的郵箱中,這篇文章主要給大家介紹了關(guān)于Java?spring?boot發(fā)送郵箱實(shí)現(xiàn)的相關(guān)資料,需要的朋友可以參考下2024-01-01Java在PowerPoint幻燈片中創(chuàng)建散點(diǎn)圖的方法
散點(diǎn)圖是通過(guò)兩組數(shù)據(jù)構(gòu)成多個(gè)坐標(biāo)點(diǎn),考察坐標(biāo)點(diǎn)的分布,判斷兩變量之間是否存在某種關(guān)聯(lián)或總結(jié)坐標(biāo)點(diǎn)的分布模式,這篇文章主要介紹了Java如何在PowerPoint幻燈片中創(chuàng)建散點(diǎn)圖,需要的朋友可以參考下2023-04-04淺析Java常用API(Scanner,Random)匿名對(duì)象
這篇文章主要介紹了Java常用API(Scanner,Random)匿名對(duì)象,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-03-03用Java設(shè)計(jì)實(shí)現(xiàn)多實(shí)例多庫(kù)查詢方式
這篇文章主要介紹了用Java設(shè)計(jì)實(shí)現(xiàn)多實(shí)例多庫(kù)查詢方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-03-03SpringBoot集成Redis實(shí)現(xiàn)驗(yàn)證碼的簡(jiǎn)單案例
本文主要介紹了SpringBoot集成Redis實(shí)現(xiàn)驗(yàn)證碼的簡(jiǎn)單案例,文中通過(guò)示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-08-08MyBatis通過(guò)JDBC數(shù)據(jù)驅(qū)動(dòng)生成的執(zhí)行語(yǔ)句問(wèn)題
這篇文章主要介紹了MyBatis通過(guò)JDBC數(shù)據(jù)驅(qū)動(dòng)生成的執(zhí)行語(yǔ)句問(wèn)題的相關(guān)資料,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友可以參考下2016-08-08關(guān)于RowBounds分頁(yè)原理、RowBounds的坑記錄
這篇文章主要介紹了關(guān)于RowBounds分頁(yè)原理、RowBounds的坑記錄,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-04-04