一個(gè)工作三年的同事,居然還搞不清深拷貝、淺拷貝...
掃描二維碼
隨時(shí)隨地手機(jī)看文章
對(duì)象拷貝在我們?nèi)粘懘a的時(shí)候基本上是剛性需求,經(jīng)常遇到,只不過很多人天天忙于寫業(yè)務(wù),忽視了一些細(xì)節(jié)問題和理解,有時(shí)候這方面一旦出了問題,就不太容易排查了。
所以本篇好好梳理一下。
注:本文已收錄于Github開源項(xiàng)目:github.com/hansonwang99/JavaCollection,里面有詳細(xì)自學(xué)編程學(xué)習(xí)路線、面試題和面經(jīng)、編程資料及系列技術(shù)文章等,資源持續(xù)更新中...
”
值類型 vs 引用類型
這兩個(gè)概念的準(zhǔn)確區(qū)分,對(duì)于深、淺拷貝問題的理解非常重要。
正如Java圣經(jīng)《Java編程思想》第二章的標(biāo)題所言,在Java中一切都可以視為對(duì)象!
所以來到Java的世界,我們要習(xí)慣用引用去操作對(duì)象。在Java中,像數(shù)組、類Class、枚舉Enum、Integer包裝類等等,就是典型的引用類型,所以操作時(shí)一般來說采用的也是引用傳遞的方式;
但是Java的語言級(jí)基礎(chǔ)數(shù)據(jù)類型,諸如int這些基本類型,操作時(shí)一般采取的則是值傳遞的方式,所以有時(shí)候也稱它為值類型。
為了便于下文的講述和舉例,我們這里先定義兩個(gè)類:Student和Major,分別表示「學(xué)生」以及「所學(xué)的專業(yè)」,二者是包含關(guān)系:
// 學(xué)生的所學(xué)專業(yè) public class Major { private String majorName; // 專業(yè)名稱 private long majorId; // 專業(yè)代號(hào) // ... 其他省略 ... }
// 學(xué)生 public class Student { private String name; // 姓名 private int age; // 年齡 private Major major; // 所學(xué)專業(yè) // ... 其他省略 ... }
賦值 vs 淺拷貝 vs 深拷貝
對(duì)象賦值
賦值是日常編程過程中最常見的操作,最簡單的比如:
Student codeSheep = new Student();
Student codePig = codeSheep;
嚴(yán)格來說,這種不能算是對(duì)象拷貝,因?yàn)榭截惖膬H僅只是引用關(guān)系,并沒有生成新的實(shí)際對(duì)象:
淺拷貝
淺拷貝屬于對(duì)象克隆方式的一種,重要的特性體現(xiàn)在這個(gè) 「淺」 字上。
比如我們?cè)噲D通過studen1實(shí)例,拷貝得到student2,如果是淺拷貝這種方式,大致模型可以示意成如下所示的樣子:
很明顯,值類型的字段會(huì)復(fù)制一份,而引用類型的字段拷貝的僅僅是引用地址,而該引用地址指向的實(shí)際對(duì)象空間其實(shí)只有一份。
一圖勝前言,我想上面這個(gè)圖已經(jīng)表現(xiàn)得很清楚了。
深拷貝
深拷貝相較于上面所示的淺拷貝,除了值類型字段會(huì)復(fù)制一份,引用類型字段所指向的對(duì)象,會(huì)在內(nèi)存中也創(chuàng)建一個(gè)副本,就像這個(gè)樣子:
原理很清楚明了,下面來看看具體的代碼實(shí)現(xiàn)吧。
淺拷貝代碼實(shí)現(xiàn)
還以上文的例子來講,我想通過student1拷貝得到student2,淺拷貝的典型實(shí)現(xiàn)方式是:讓被復(fù)制對(duì)象的類實(shí)現(xiàn)Cloneable接口,并重寫clone()方法即可。
以上面的Student類拷貝為例:
public class Student implements Cloneable { private String name; // 姓名 private int age; // 年齡 private Major major; // 所學(xué)專業(yè) @Override public Object clone() throws CloneNotSupportedException { return super.clone(); } // ... 其他省略 ... }
然后我們寫個(gè)測(cè)試代碼,一試便知:
public class Test { public static void main(String[] args) throws CloneNotSupportedException { Major m = new Major("計(jì)算機(jī)科學(xué)與技術(shù)",666666); Student student1 = new Student( "CodeSheep", 18, m ); // 由 student1 拷貝得到 student2 Student student2 = (Student) student1.clone(); System.out.println( student1 == student2 ); System.out.println( student1 ); System.out.println( student2 ); System.out.println( "\n" ); // 修改student1的值類型字段 student1.setAge( 35 ); // 修改student1的引用類型字段 m.setMajorName( "電子信息工程" ); m.setMajorId( 888888 ); System.out.println( student1 ); System.out.println( student2 ); } }
運(yùn)行得到如下結(jié)果:
從結(jié)果可以看出:
- student1==student2打印false,說明clone()方法的確克隆出了一個(gè)新對(duì)象;
- 修改值類型字段并不影響克隆出來的新對(duì)象,符合預(yù)期;
- 而修改了student1內(nèi)部的引用對(duì)象,克隆對(duì)象student2也受到了波及,說明內(nèi)部還是關(guān)聯(lián)在一起的
深拷貝代碼實(shí)現(xiàn)
深度遍歷式拷貝
雖然clone()方法可以完成對(duì)象的拷貝工作,但是注意:clone()方法默認(rèn)是淺拷貝行為,就像上面的例子一樣。若想實(shí)現(xiàn)深拷貝需覆寫clone()方法實(shí)現(xiàn)引用對(duì)象的深度遍歷式拷貝,進(jìn)行地毯式搜索。
所以對(duì)于上面的例子,如果想實(shí)現(xiàn)深拷貝,首先需要對(duì)更深一層次的引用類Major做改造,讓其也實(shí)現(xiàn)Cloneable接口并重寫clone()方法:
public class Major implements Cloneable { @Override protected Object clone() throws CloneNotSupportedException { return super.clone(); } // ... 其他省略 ... }
其次我們還需要在頂層的調(diào)用類中重寫clone方法,來調(diào)用引用類型字段的clone()方法實(shí)現(xiàn)深度拷貝,對(duì)應(yīng)到本文那就是Student類:
public class Student implements Cloneable { @Override public Object clone() throws CloneNotSupportedException { Student student = (Student) super.clone(); student.major = (Major) major.clone(); // 重要?。?! return student; } // ... 其他省略 ... }
這時(shí)候上面的測(cè)試用例不變,運(yùn)行可得結(jié)果:
很明顯,這時(shí)候student1和student2兩個(gè)對(duì)象就完全獨(dú)立了,不受互相的干擾。
利用反序列化實(shí)現(xiàn)深拷貝
記得在前文《序列化/反序列化,我忍你很久了》中就已經(jīng)詳細(xì)梳理和總結(jié)了「序列化和反序列化」這個(gè)知識(shí)點(diǎn)了。
利用反序列化技術(shù),我們也可以從一個(gè)對(duì)象深拷貝出另一個(gè)復(fù)制對(duì)象,而且這貨在解決多層套娃式的深拷貝問題時(shí)效果出奇的好。
所以我們這里改造一下Student類,讓其clone()方法通過序列化和反序列化的方式來生成一個(gè)原對(duì)象的深拷貝副本:
public class Student implements Serializable { private String name; // 姓名 private int age; // 年齡 private Major major; // 所學(xué)專業(yè) public Student clone() { try { // 將對(duì)象本身序列化到字節(jié)流 ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); ObjectOutputStream objectOutputStream = new ObjectOutputStream( byteArrayOutputStream ); objectOutputStream.writeObject( this ); // 再將字節(jié)流通過反序列化方式得到對(duì)象副本 ObjectInputStream objectInputStream = new ObjectInputStream( new ByteArrayInputStream( byteArrayOutputStream.toByteArray() ) ); return (Student) objectInputStream.readObject(); } catch (IOException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } return null; } // ... 其他省略 ... }
當(dāng)然這種情況下要求被引用的子類(比如這里的Major類)也必須是可以序列化的,即實(shí)現(xiàn)了Serializable接口:
public class Major implements Serializable { // ... 其他省略 ... }
這時(shí)候測(cè)試用例完全不變,直接運(yùn)行,也可以得到如下結(jié)果:
很明顯,這時(shí)候student1和student2兩個(gè)對(duì)象也是完全獨(dú)立的,不受互相的干擾,深拷貝完成。
后 記
好了,關(guān)于「深拷貝」和「淺拷貝」這個(gè)問題這次就聊到這里吧。本以為這篇會(huì)很快寫完,結(jié)果又扯出了這么多東西,不過這樣一梳理、一串聯(lián),感覺還是清晰了不少。
—————END—————
免責(zé)聲明:本文內(nèi)容由21ic獲得授權(quán)后發(fā)布,版權(quán)歸原作者所有,本平臺(tái)僅提供信息存儲(chǔ)服務(wù)。文章僅代表作者個(gè)人觀點(diǎn),不代表本平臺(tái)立場(chǎng),如有問題,請(qǐng)聯(lián)系我們,謝謝!