Java對(duì)象進(jìn)行深拷貝的五種方法實(shí)例代碼
前言
在 Java 語(yǔ)言里,當(dāng)我們需要拷貝一個(gè)對(duì)象時(shí),有兩種類型的拷貝:淺拷貝與深拷貝。淺拷貝只是拷貝了源對(duì)象的地址,所以源對(duì)象的值發(fā)生變化時(shí),拷貝對(duì)象的值也會(huì)發(fā)生變化。而深拷貝則是拷貝了源對(duì)象的所有值,所以即使源對(duì)象的值發(fā)生變化時(shí),拷貝對(duì)象的值也不會(huì)改變。如下圖描述:
了解了淺拷貝和深拷貝的區(qū)別之后,本篇博客將教大家?guī)追N深拷貝的方法。
拷貝對(duì)象
首先,我們定義一下需要拷貝的簡(jiǎn)單對(duì)象。
/** * 用戶 */ public class User { private String name; private Address address; // constructors, getters and setters } /** * 地址 */ public class Address { private String city; private String country; // constructors, getters and setters }
如上述代碼,我們定義了一個(gè)User用戶類,包含name姓名,和address地址,其中address并不是字符串,而是另一個(gè)Address類,包含country國(guó)家和city城市。構(gòu)造方法和成員變量的get()、set()方法此處我們省略不寫。接下來(lái)我們將詳細(xì)描述如何深拷貝User對(duì)象。
方法一 構(gòu)造函數(shù)
我們可以通過(guò)在調(diào)用構(gòu)造函數(shù)進(jìn)行深拷貝,形參如果是基本類型和字符串則直接賦值,如果是對(duì)象則重新new一個(gè)。
測(cè)試用例
@Test public void constructorCopy() { Address address = new Address("安徽", "中國(guó)"); User user = new User("TM", address); // 調(diào)用構(gòu)造函數(shù)時(shí)進(jìn)行深拷貝 User copyUser = new User(user.getName(), new Address(address.getCity(), address.getCountry())); // 修改源對(duì)象的值 user.getAddress().setCity("合肥"); // 檢查兩個(gè)對(duì)象的值不同 assertNotSame(user.getAddress().getCity(), copyUser.getAddress().getCity()); }
方法二 重載clone()方法
Object父類有個(gè)clone()的拷貝方法,不過(guò)它是protected類型的,我們需要重寫它并修改為public類型。除此之外,子類還需要實(shí)現(xiàn)Cloneable接口來(lái)告訴JVM這個(gè)類是可以拷貝的。
重寫代碼
讓我們修改一下User類,Address類,實(shí)現(xiàn)Cloneable接口,使其支持深拷貝。
/** * 地址 */ public class Address implements Cloneable { private String city; private String country; // constructors, getters and setters @Override public Address clone() throws CloneNotSupportedException { return (Address) super.clone(); } }
/** * 用戶 */ public class User implements Cloneable { private String name; private Address address; // constructors, getters and setters @Override public User clone() throws CloneNotSupportedException { User user = (User) super.clone(); user.setAddress(this.address.clone()); return user; } }
需要注意的是,super.clone()其實(shí)是淺拷貝,所以在重寫User類的clone()方法時(shí),address對(duì)象需要調(diào)用address.clone()重新賦值。
測(cè)試用例
@Test public void cloneCopy() throws CloneNotSupportedException { Address address = new Address("安徽", "中國(guó)"); User user = new User("TM", address); // 調(diào)用clone()方法進(jìn)行深拷貝 User copyUser = user.clone(); // 修改源對(duì)象的值 user.getAddress().setCity("合肥"); // 檢查兩個(gè)對(duì)象的值不同 assertNotSame(user.getAddress().getCity(), copyUser.getAddress().getCity()); }
方法三 Apache Commons Lang序列化
Java提供了序列化的能力,我們可以先將源對(duì)象進(jìn)行序列化,再反序列化生成拷貝對(duì)象。但是,使用序列化的前提是拷貝的類(包括其成員變量)需要實(shí)現(xiàn)Serializable接口。Apache Commons Lang包對(duì)Java序列化進(jìn)行了封裝,我們可以直接使用它。
重寫代碼
讓我們修改一下User類,Address類,實(shí)現(xiàn)Serializable接口,使其支持序列化。
/** * 地址 */ public class Address implements Serializable { private String city; private String country; // constructors, getters and setters }
/** * 用戶 */ public class User implements Serializable { private String name; private Address address; // constructors, getters and setters }
測(cè)試用例
@Test public void serializableCopy() { Address address = new Address("安徽", "中國(guó)"); User user = new User("TM", address); // 使用Apache Commons Lang序列化進(jìn)行深拷貝 User copyUser = (User) SerializationUtils.clone(user); // 修改源對(duì)象的值 user.getAddress().setCity("合肥"); // 檢查兩個(gè)對(duì)象的值不同 assertNotSame(user.getAddress().getCity(), copyUser.getAddress().getCity()); }
方法四 Gson序列化
Gson可以將對(duì)象序列化成JSON,也可以將JSON反序列化成對(duì)象,所以我們可以用它進(jìn)行深拷貝。
測(cè)試用例
@Test public void gsonCopy() { Address address = new Address("安徽", "中國(guó)"); User user = new User("TM", address); // 使用Gson序列化進(jìn)行深拷貝 Gson gson = new Gson(); User copyUser = gson.fromJson(gson.toJson(user), User.class); // 修改源對(duì)象的值 user.getAddress().setCity("合肥"); // 檢查兩個(gè)對(duì)象的值不同 assertNotSame(user.getAddress().getCity(), copyUser.getAddress().getCity()); }
方法五 Jackson序列化
Jackson與Gson相似,可以將對(duì)象序列化成JSON,明顯不同的地方是拷貝的類(包括其成員變量)需要有默認(rèn)的無(wú)參構(gòu)造函數(shù)。
重寫代碼
讓我們修改一下User類,Address類,實(shí)現(xiàn)默認(rèn)的無(wú)參構(gòu)造函數(shù),使其支持Jackson。
/** * 用戶 */ public class User { private String name; private Address address; // constructors, getters and setters public User() { } }
/** * 地址 */ public class Address { private String city; private String country; // constructors, getters and setters public Address() { } }
測(cè)試用例
@Test public void jacksonCopy() throws IOException { Address address = new Address("安徽", "中國(guó)"); User user = new User("TM", address); // 使用Jackson序列化進(jìn)行深拷貝 ObjectMapper objectMapper = new ObjectMapper(); User copyUser = objectMapper.readValue(objectMapper.writeValueAsString(user), User.class); // 修改源對(duì)象的值 user.getAddress().setCity("合肥"); // 檢查兩個(gè)對(duì)象的值不同 assertNotSame(user.getAddress().getCity(), copyUser.getAddress().getCity()); }
總結(jié)
說(shuō)了這么多深拷貝的實(shí)現(xiàn)方法,哪一種方法才是最好的呢?最簡(jiǎn)單的判斷就是根據(jù)拷貝的類(包括其成員變量)是否提供了深拷貝的構(gòu)造函數(shù)、是否實(shí)現(xiàn)了Cloneable接口、是否實(shí)現(xiàn)了Serializable接口、是否實(shí)現(xiàn)了默認(rèn)的無(wú)參構(gòu)造函數(shù)來(lái)進(jìn)行選擇。如果需要詳細(xì)的考慮,則可以參考下面的表格:
深拷貝方法 | 優(yōu)點(diǎn) | 缺點(diǎn) |
---|---|---|
構(gòu)造函數(shù) | 1.底層實(shí)現(xiàn)簡(jiǎn)單 2.不需要引入第三方包 3.系統(tǒng)開銷小 4.對(duì)拷貝類沒(méi)有要求,不需要實(shí)現(xiàn)額外接口和方法 | 1.可用性差,每次新增成員變量都需要新增新的拷貝構(gòu)造函數(shù) |
重載clone()方法 | 1.底層實(shí)現(xiàn)較簡(jiǎn)單 2.不需要引入第三方包 3.系統(tǒng)開銷小 | 1.可用性較差,每次新增成員變量可能需要修改clone()方法 2.拷貝類(包括其成員變量)需要實(shí)現(xiàn)Cloneable接囗 |
Apache CommonsLang序列化 | 1.可用性強(qiáng),新增成員變量不需要修改拷貝方法 | 1.底層實(shí)現(xiàn)較復(fù)雜 2.需要引入Apache Commons Lang第二方JAR包 3.拷貝類(包括其成員變量)需要實(shí)現(xiàn)Serializable接口 4.序列化與反序列化存在一定的系統(tǒng)開銷 |
Gson序列化 | 1.可用性強(qiáng),新增成員變量不需要修改拷貝方法 2.對(duì)拷貝類沒(méi)有要求,不需要實(shí)現(xiàn)額外接口和方法 | 1.底層實(shí)現(xiàn)復(fù)雜 2.需要引入Gson第三方JAR包 3.序列化與反序列化存在一定的系統(tǒng)開銷 |
Jackson序列化 | 1.可用性強(qiáng),新增成員變量不需要修改拷貝方法 | 1.底層實(shí)現(xiàn)復(fù)雜 2.需要引入Jackson第三方JAR包 3.拷貝類(包括其成員變量)需要實(shí)現(xiàn)默認(rèn)的無(wú)參構(gòu)造函數(shù) 4.序列化與反序列化存在一定的系統(tǒng)開 |
到此這篇關(guān)于Java對(duì)象進(jìn)行深拷貝的五種方法的文章就介紹到這了,更多相關(guān)Java對(duì)象深拷貝內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
SpringBoot Mybatis如何配置多數(shù)據(jù)源并分包
這篇文章主要介紹了SpringBoot Mybatis如何配置多數(shù)據(jù)源并分包,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-05-05JVM中對(duì)象的創(chuàng)建與OOP-Klass模型
這篇文章主要介紹了JVM中對(duì)象的創(chuàng)建與OOP-Klass模型,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-09-09JavaFX實(shí)現(xiàn)簡(jiǎn)單日歷效果
這篇文章主要為大家詳細(xì)介紹了JavaFX實(shí)現(xiàn)簡(jiǎn)單日歷效果,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-11-11詳解MyBatis的getMapper()接口、resultMap標(biāo)簽、Alias別名、 盡量提取sql列、動(dòng)態(tài)操作
這篇文章主要介紹了詳解MyBatis的getMapper()接口、resultMap標(biāo)簽、Alias別名、 盡量提取sql列、動(dòng)態(tài)操作的相關(guān)資料,需要的朋友可以參考下2016-08-08java 數(shù)據(jù)結(jié)構(gòu)基本算法希爾排序
這篇文章主要介紹了數(shù)據(jù)結(jié)構(gòu)基本算法希爾排序的相關(guān)資料,需要的朋友可以參考下2017-08-08Java JDK動(dòng)態(tài)代理在攔截器和聲明式接口中的應(yīng)用小結(jié)
Java動(dòng)態(tài)代理技術(shù)通過(guò)反射機(jī)制在運(yùn)行時(shí)動(dòng)態(tài)生成代理類,實(shí)現(xiàn)對(duì)目標(biāo)對(duì)象方法的攔截和增強(qiáng),本文給大家介紹Java JDK動(dòng)態(tài)代理在攔截器和聲明式接口中的應(yīng)用小結(jié),感興趣的朋友跟隨小編一起看看吧2025-01-01Java8如何利用Lambda快速生成map、多層嵌套map
這篇文章主要介紹了Java8如何利用Lambda快速生成map、多層嵌套map問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-09-09