當(dāng)前位置:首頁 > 公眾號(hào)精選 > 程序員小灰
[導(dǎo)讀]對(duì)象拷貝在我們?nèi)粘懘a的時(shí)候基本上是剛性需求,經(jīng)常遇到,只不過很多人天天忙于寫業(yè)務(wù),忽視了一些細(xì)節(jié)問題和理解,有時(shí)候這方面一旦出了問題,就不太容易排查了。 所以本篇好好梳理一下。 注:本文已收錄于Github開源項(xiàng)目:github.com/hansonwang99/Java



對(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)系我們,謝謝!

本站聲明: 本文章由作者或相關(guān)機(jī)構(gòu)授權(quán)發(fā)布,目的在于傳遞更多信息,并不代表本站贊同其觀點(diǎn),本站亦不保證或承諾內(nèi)容真實(shí)性等。需要轉(zhuǎn)載請(qǐng)聯(lián)系該專欄作者,如若文章內(nèi)容侵犯您的權(quán)益,請(qǐng)及時(shí)聯(lián)系本站刪除。
關(guān)閉
關(guān)閉