什么是 Java 对象深拷贝?面试必问!
作者丨吳大山
wudashan.com/2018/10/14/Java-Deep-Copy
介紹
在Java語言里,當(dāng)我們需要拷貝一個(gè)對(duì)象時(shí),有兩種類型的拷貝:淺拷貝與深拷貝。淺拷貝只是拷貝了源對(duì)象的地址,所以源對(duì)象的值發(fā)生變化時(shí),拷貝對(duì)象的值也會(huì)發(fā)生變化。而深拷貝則是拷貝了源對(duì)象的所有值,所以即使源對(duì)象的值發(fā)生變化時(shí),拷貝對(duì)象的值也不會(huì)改變。如下圖描述:
了解了淺拷貝和深拷貝的區(qū)別之后,本篇博客將教大家?guī)追N深拷貝的方法。
拷貝對(duì)象
首先,我們定義一下需要拷貝的簡單對(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國家和city城市。構(gòu)造方法和成員變量的get()、set()方法此處我們省略不寫。接下來我們將詳細(xì)描述如何深拷貝User對(duì)象。
方法一 構(gòu)造函數(shù)
我們可以通過在調(diào)用構(gòu)造函數(shù)進(jìn)行深拷貝,形參如果是基本類型和字符串則直接賦值,如果是對(duì)象則重新new一個(gè)。
測(cè)試用例
@Test public?void?constructorCopy()?{Address?address?=?new?Address("杭州",?"中國");User?user?=?new?User("大山",?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()的拷貝方法,不過它是protected類型的,我們需要重寫它并修改為public類型。除此之外,子類還需要實(shí)現(xiàn)Cloneable接口來告訴JVM這個(gè)類是可以拷貝的。
重寫代碼
讓我們修改一下User類,Address類,實(shí)現(xiàn)Cloneable接口,使其支持深拷貝。
/***?地址*/ public?class?Address?implements?Cloneable?{private?String?city;private?String?country;//?constructors,?getters?and?setters@Overridepublic?Address?clone()?throws?CloneNotSupportedException?{return?(Address)?super.clone();}} /***?用戶*/ public?class?User?implements?Cloneable?{private?String?name;private?Address?address;//?constructors,?getters?and?setters@Overridepublic?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("杭州",?"中國");User?user?=?new?User("大山",?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("杭州",?"中國");User?user?=?new?User("大山",?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("杭州",?"中國");User?user?=?new?User("大山",?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)的無參構(gòu)造函數(shù)。
重寫代碼
讓我們修改一下User類,Address類,實(shí)現(xiàn)默認(rèn)的無參構(gòu)造函數(shù),使其支持Jackson。
/***?用戶*/ public?class?User?{private?String?name;private?Address?address;//?constructors,?getters?and?setterspublic?User()?{}} /***?地址*/ public?class?Address?{private?String?city;private?String?country;//?constructors,?getters?and?setterspublic?Address()?{}}測(cè)試用例
@Test public?void?jacksonCopy()?throws?IOException?{Address?address?=?new?Address("杭州",?"中國");User?user?=?new?User("大山",?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é)
說了這么多深拷貝的實(shí)現(xiàn)方法,哪一種方法才是最好的呢?最簡單的判斷就是根據(jù)拷貝的類(包括其成員變量)是否提供了深拷貝的構(gòu)造函數(shù)、是否實(shí)現(xiàn)了Cloneable接口、是否實(shí)現(xiàn)了Serializable接口、是否實(shí)現(xiàn)了默認(rèn)的無參構(gòu)造函數(shù)來進(jìn)行選擇。如果需要詳細(xì)的考慮,則可以參考下面的表格:
總結(jié)
以上是生活随笔為你收集整理的什么是 Java 对象深拷贝?面试必问!的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 原子变量、锁、内存屏障,写得非常好!
- 下一篇: 为什么 Java 线程没有 Runnin