一個(gè)工作三年的同事,居然還搞不清深拷貝、淺拷貝...

文章來源于公眾號(hào)CodeSheep ,作者CodeSheep

image

對(duì)象拷貝在我們?nèi)粘懘a的時(shí)候基本上是剛性需求,經(jīng)常遇到,只不過很多人天天忙于寫業(yè)務(wù),忽視了一些細(xì)節(jié)問題和理解,有時(shí)候這方面一旦出了問題,就不太容易排查了。

所以本篇好好梳理一下。

注:本文已收錄于Github開源項(xiàng)目:github.com/hansonwang99/JavaCollection


值類型 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è)類:StudentMajor,分別表示「學(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è)

    // ... 其他省略 ...
}
image

賦值 vs 淺拷貝 vs 深拷貝

對(duì)象賦值

賦值是日常編程過程中最常見的操作,最簡單的比如:

Student codeSheep = new Student();
Student codePig = codeSheep;

嚴(yán)格來說,這種不能算是對(duì)象拷貝,因?yàn)榭截惖膬H僅只是引用關(guān)系,并沒有生成新的實(shí)際對(duì)象:

image

淺拷貝

淺拷貝屬于對(duì)象克隆方式的一種,重要的特性體現(xiàn)在這個(gè) 「淺」 字上。

比如我們?cè)噲D通過studen1實(shí)例,拷貝得到student2,如果是淺拷貝這種方式,大致模型可以示意成如下所示的樣子:

image

很明顯,值類型的字段會(huì)復(fù)制一份,而引用類型的字段拷貝的僅僅是引用地址,而該引用地址指向的實(shí)際對(duì)象空間其實(shí)只有一份。

一圖勝前言,我想上面這個(gè)圖已經(jīng)表現(xiàn)得很清楚了。

深拷貝

深拷貝相較于上面所示的淺拷貝,除了值類型字段會(huì)復(fù)制一份,引用類型字段所指向的對(duì)象,會(huì)在內(nèi)存中也創(chuàng)建一個(gè)副本,就像這個(gè)樣子:

image

原理很清楚明了,下面來看看具體的代碼實(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è)測試代碼,一試便知:

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é)果:

image

從結(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í)候上面的測試用例不變,運(yùn)行可得結(jié)果:

image

很明顯,這時(shí)候student1student2兩個(gè)對(duì)象就完全獨(dú)立了,不受互相的干擾。

利用反序列化實(shí)現(xià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í)候測試用例完全不變,直接運(yùn)行,也可以得到如下結(jié)果:

image

很明顯,這時(shí)候student1student2兩個(gè)對(duì)象也是完全獨(dú)立的,不受互相的干擾,深拷貝完成。


后 記

好了,關(guān)于「深拷貝」和「淺拷貝」這個(gè)問題這次就聊到這里吧。本以為這篇會(huì)很快寫完,結(jié)果又扯出了這么多東西,不過這樣一梳理、一串聯(lián),感覺還是清晰了不少。

就這樣吧,下篇見。

注:本文已收錄于Github開源項(xiàng)目:github.com/hansonwang99/JavaCollection


每天進(jìn)步一點(diǎn)點(diǎn)

慢一點(diǎn)才能更快

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容