這一篇足夠讓你理解深拷貝和淺拷貝(詳細)

寫在前面

如果覺得有所幫助,記得點個關注和點個贊哦,非常感謝支持。
任何變成語言中,其實都有淺拷貝和深拷貝的概念,Java 中也不例外。在對一個現有的對象進行拷貝操作的時候,是有淺拷貝和深拷貝之分的,他們在實際使用中,區別很大,如果對其進行混淆,可能會引發一些難以排查的問題。Java 中的數據類型分爲基本數據類型和引用數據類型。對於這兩種數據類型,在進行賦值操作、用作方法參數或返回值時,會有值傳遞和引用(地址)傳遞的差別。

什麼是淺拷貝和深拷貝

首先需要明白,淺拷貝和深拷貝都是針對一個已有對象的操作。那先來看看淺拷貝和深拷貝的概念。上面講了,在 Java 中,除了基本數據類型(元類型)之外,還存在類的實例對象這個引用數據類型。而一般使用 『 = 』號做賦值操作的時候。對於基本數據類型,實際上是拷貝的它的值,但是對於對象而言,其實賦值的只是這個對象的引用,將原對象的引用傳遞過去,他們實際上還是指向的同一個對象。

而淺拷貝和深拷貝就是在這個基礎之上做的區分,如果在拷貝這個對象的時候,只對是基本數據類型的對象屬性進行了拷貝,而對是引用數據類型的對象屬性,只是進行了引用的傳遞,而沒有真實的創建一個新的對象,則認爲是淺拷貝。反之,在對是引用數據類型的對象屬性進行拷貝的時候,創建了一個新的對象,並且複製其內的成員變量,則認爲是深拷貝。這段話要仔細理解清楚,如果我們對象直接用“=”賦值,那麼只是對象拷貝,不交淺拷貝和深拷貝。所以就應該瞭解了,所謂的淺拷貝和深拷貝,只是在拷貝對象的時候,針對對象所擁有的屬性拷貝的說法,而且一定是使用了clone纔算是淺拷貝和深拷貝(下面講解代碼的時候還會進行說明)。

  • 淺拷貝:對基本數據類型進行值傳遞,對引用數據類型進行引用傳遞般的拷貝,此爲淺拷貝。淺拷貝是按位拷貝對象,它會創建一個新對象,這個對象有着原始對象屬性值的一份精確拷貝。如果屬性是基本類型,拷貝的就是基本類型的值;如果屬性是內存地址(引用類型),拷貝的就是內存地址 ,因此如果其中一個對象改變了這個地址,就會影響到另一個對象。即默認拷貝構造函數只是對對象進行淺拷貝複製(逐個成員依次拷貝),即只複製對象空間而不復制資源。
    在這裏插入圖片描述
  • 深拷貝:對基本數據類型進行值傳遞,對引用數據類型,創建一個新的對象,並複製其內容,此爲深拷貝。
    在這裏插入圖片描述

Java 中的拷貝

對象拷貝

  • 我們所知道的直接賦值,即使用“=”,對於基本數據類型而言,都是進行值傳遞;
  • 對於對象進行直接賦值,即使用“=”,都是引用傳遞

淺拷貝特點

  • 對於基本數據類型的成員對象,因爲基礎數據類型是值傳遞的,所以是直接將屬性值賦值給新的對象。基礎類型的拷貝,其中一個對象修改該值,不會影響另外一個。
  • 對於引用類型的成員對象,比如數組或者類對象,因爲引用類型是引用傳遞,所以淺拷貝只是把內存地址賦值給了成員變量,它們指向了同一內存空間。改變其中一個,會對另外一個也產生影響。

深拷貝特點

  • 對於基本數據類型的成員對象,因爲基礎數據類型是值傳遞的,所以是直接將屬性值賦值給新的對象。基礎類型的拷貝,其中一個對象修改該值,不會影響另外一個(和淺拷貝一樣)。
  • 對於引用類型的成員對象,比如數組或者類對象,深拷貝會新建一個對象空間,然後拷貝里面的內容,所以它們指向了不同的內存空間。改變其中一個,不會對另外一個也產生影響。
  • 對於有多層對象的成員對象,每個對象都需要實現 Cloneable 並重寫 clone() 方法,進而實現了對象的串行層層拷貝。
  • 深拷貝相比於淺拷貝速度較慢並且花銷較大。

通過代碼實現進一步理解

上面的概念講解還不理解的話,不要着急,這裏我們使用代碼來進行認識和思考,首先我們先要創建兩個類,來爲後面的講解做準備,兩個類分別是SubjectStudent,代碼如下

public class Student {
    private Subject subject;
    private String name;
    private int age;

    public Subject getSubject() {
        return subject;
    }

    public void setSubject(Subject subject) {
        this.subject = subject;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
    

    @Override
    public String toString() {
        return "[Student: " + this.hashCode() + ",subject:" + subject + ",name:" + name + ",age:" + age + "]";
    }
}

public class Subject {

    private String name;

    public Subject(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "[Subject: " + this.hashCode() + ",name:" + name + "]";
    }
}

直接賦值

有了上面的代碼之後呢,我們可以來體驗一下直接賦值,我們知道,直接賦值對於基本數據類型而言,就是值傳遞,對於引用類型而言就是引用傳遞,比如,我們直接如下這樣

public class Main {
    public static void main(String[] args) {
        Subject subjectA = new Subject("hahah");

        Student studentA = new Student();
        studentA.setAge(18);
        studentA.setName("qqqqq");
        studentA.setSubject(subjectA);

        //直接賦值,不使用clone進行深淺拷貝
        Student studentB = studentA;
        studentA.setAge(20);
        System.out.println(studentA);
        System.out.println(studentB);
    }
}

在這裏插入圖片描述
我們看到輸出結果,發現哈希碼都是一樣的,說明 studentAstudentB 是指向了同一個對象,所以在更改age屬性的時候,同時修改了。

淺拷貝

上面的操作叫做對象拷貝,那麼我們接下來開始講講淺拷貝的代碼,在講解淺拷貝的代碼之前,我們需要把Student類進行改造一下,讓它實現Cloneable接口,並重寫clone方法,Student改寫之後代碼如下

public class Student implements Cloneable {
    private Subject subject;
    private String name;
    private int age;

    public Subject getSubject() {
        return subject;
    }

    public void setSubject(Subject subject) {
        this.subject = subject;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        //淺拷貝,直接調用父類的clone()方法
        return super.clone();
    }

    @Override
    public String toString() {
        return "[Student: " + this.hashCode() + ",subject:" + subject + ",name:" + name + ",age:" + age + "]";
    }
}

這個時候,我們就可以調用Studentclone方法,來完成對象的淺拷貝,我們只需要在上面的Main方法中,講直接賦值的對象拷貝,改成clone的拷貝賦值就可以了

public class Main {
    public static void main(String[] args) {
        Subject subjectA = new Subject("hahah");

        Student studentA = new Student();
        studentA.setAge(18);
        studentA.setName("qqqqq");
        studentA.setSubject(subjectA);

        //淺拷貝,使用clone進行淺拷貝
        Student studentB = null;
        try {
            studentB = (Student)studentA.clone();
        }catch (CloneNotSupportedException e){
            e.printStackTrace();
        }
        studentA.setAge(20);
        studentA.getSubject().setName("heihei");
        System.out.println(studentA);
        System.out.println(studentB);
    }
}

在這裏插入圖片描述
我們看到輸出結果,發現哈希碼不一樣的,說明 studentAstudentB 不是指向了同一個對象,所以在studentA更改age屬性的時候,只有studentA修改了。但是在修改studentA的subject的時候,發現了沒有,兩個同時修改了,studentA和studentB兩個的subject屬性的哈希碼是一樣的,所以這也就是淺拷貝,淺拷貝對於對象的基本類型屬性,是值傳遞,而對於引用類型的屬性,是引用傳遞。

深拷貝

上面的操作叫做淺拷貝,那麼我們接下來開始講講深拷貝的代碼,在講解深拷貝的代碼之前,我們需要接着把Student類的clone方法進行改造一下,不僅如此,我們還需要把Subject類改造,讓它實現Cloneable接口,重寫clone方法,代碼如下

public class Subject implements Cloneable {

    private String name;

    public Subject(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }

    @Override
    public String toString() {
        return "[Subject: " + this.hashCode() + ",name:" + name + "]";
    }
}

public class Student implements Cloneable {
    //引用類型
    private Subject subject;
    //基礎數據類型
    private String name;
    private int age;

    public Subject getSubject() {
        return subject;
    }

    public void setSubject(Subject subject) {
        this.subject = subject;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        //淺拷貝,直接調用父類的clone()方法
        Student student = (Student) super.clone();
        student.subject = (Subject) subject.clone();
        return student;
    }

    @Override
    public String toString() {
        return "[Student: " + this.hashCode() + ",subject:" + subject + ",name:" + name + ",age:" + age + "]";
    }
}

發現了沒有,要想進行深拷貝,我們必須把拷貝對象裏面的,所有引用類型屬性的對象,都實現Cloneable接口,一層一層的屬性對象,只要是引用類型的對象,都要實現Cloneable接口,這樣才能實現深拷貝,我們接着使用上面淺拷貝的Main方法來檢測一下深拷貝的結果

public class Main {
    public static void main(String[] args) {
        Subject subjectA = new Subject("hahah");

        Student studentA = new Student();
        studentA.setAge(18);
        studentA.setName("qqqqq");
        studentA.setSubject(subjectA);

        //深拷貝,使用clone進行深拷貝
        Student studentB = null;
        try {
            studentB = (Student)studentA.clone();
        }catch (CloneNotSupportedException e){
            e.printStackTrace();
        }
        studentA.setAge(20);
        studentA.getSubject().setName("heihei");
        System.out.println(studentA);
        System.out.println(studentB);
    }
}

在這裏插入圖片描述
由輸出結果可見,深拷貝後,不管是基礎數據類型還是引用類型的成員變量,修改其值都不會相互造成影響。

通過序列化實現深拷貝

也可以通過序列化來實現深拷貝。序列化是幹什麼的?它將整個對象圖寫入到一個持久化存儲文件中並且當需要的時候把它讀取回來, 這意味着當你需要把它讀取回來時你需要整個對象圖的一個拷貝。這就是當你深拷貝一個對象時真正需要的東西。請注意,當你通過序列化進行深拷貝時,必須確保對象圖中所有類都是可序列化的。首先我們先對StudentSubject進行改造,這兩個類都要實現Serializable接口,代碼如下:

import java.io.Serializable;

public class Student implements Serializable {
    //引用類型
    private Subject subject;
    //基礎數據類型
    private String name;
    private int age;

    public Subject getSubject() {
        return subject;
    }

    public void setSubject(Subject subject) {
        this.subject = subject;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "[Student: " + this.hashCode() + ",subject:" + subject + ",name:" + name + ",age:" + age + "]";
    }
}

import java.io.Serializable;

public class Subject implements Serializable {

    private String name;

    public Subject(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "[Subject: " + this.hashCode() + ",name:" + name + "]";
    }
}

我們完成了這兩個類的改造之後呢,我們接下來在Main方法中測試他們,Main方法的內容如下:

public class Main {
    public static void main(String[] args) throws Exception {
        Subject subjectA = new Subject("hahah");

        Student studentA = new Student();
        studentA.setAge(18);
        studentA.setName("qqqqq");
        studentA.setSubject(subjectA);

        // 通過序列化實現深拷貝
        ObjectOutputStream objectOutputStream = null;
        ObjectInputStream objectInputStream = null;
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        objectOutputStream = new ObjectOutputStream(outputStream);
        // 序列化以及傳遞這個對象
        objectOutputStream.writeObject(studentA);
        objectOutputStream.flush();
        ByteArrayInputStream inputStream = new ByteArrayInputStream(outputStream.toByteArray());
        objectInputStream = new ObjectInputStream(inputStream);
        // 返回新的對象
        Student studentB = (Student)objectInputStream.readObject();

        studentB.setAge(20);
        studentB.setName("wwwww");
        studentB.getSubject().setName("heihei");
        System.out.println(studentA);
        System.out.println(studentB);
    }
}

在這裏插入圖片描述

延遲拷貝

延遲拷貝是淺拷貝和深拷貝的一個組合,實際上很少會使用。 當最開始拷貝一個對象時,會使用速度較快的淺拷貝,還會使用一個計數器來記錄有多少對象共享這個數據。當程序想要修改原始的對象時,它會決定數據是否被共享(通過檢查計數器)並根據需要進行深拷貝。延遲拷貝從外面看起來就是深拷貝,但是只要有可能它就會利用淺拷貝的速度。當原始對象中的引用不經常改變的時候可以使用延遲拷貝。由於存在計數器,效率下降很高,但只是常量級的開銷。而且,在某些情況下,循環引用會導致一些問題。

總結

如果對象的屬性全是基本類型的,那麼可以使用淺拷貝,但是如果對象有引用屬性,那就要基於具體的需求來選擇淺拷貝還是深拷貝。我的意思是如果對象引用任何時候都不會被改變,那麼沒必要使用深拷貝,只需要使用淺拷貝就行了。如果對象引用經常改變,那麼就要使用深拷貝。沒有一成不變的規則,一切都取決於具體需求。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章