死磕Java面試系列:深拷貝與淺拷貝的實現原理

2022-11-07 12:00:12

深拷貝與淺拷貝的問題,也是面試中的常客。雖然大家都知道兩者表現形式不同點在哪裡,但是很少去深究其底層原理,也不知道怎麼才能優雅的實現一個深拷貝。其實工作中也常常需要實現深拷貝,今天一燈就帶大家一塊深入剖析一下深拷貝與淺拷貝的實現原理,並手把手教你怎麼優雅的實現深拷貝。

1. 什麼是深拷貝與淺拷貝

淺拷貝: 只拷貝棧記憶體中的資料,不拷貝堆記憶體中資料。

深拷貝: 既拷貝棧記憶體中的資料,又拷貝堆記憶體中的資料。

2. 淺拷貝的實現原理

由於淺拷貝只拷貝了棧記憶體中資料,棧記憶體中儲存的都是基本資料型別,堆記憶體中儲存了陣列、參照資料型別等。

使用程式碼驗證一下:

想要實現clone功能,需要實現 Cloneable 介面,並重寫 clone 方法。

  1. 先建立一個使用者類
// 使用者的實體類,用作驗證
public class User implements Cloneable {
    private String name;
    
    // 每個使用者都有一個工作
    private Job job;

    public String getName() {
        return name;
    }

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

    public Job getJob() {
        return job;
    }

    public void setJob(Job job) {
        this.job = job;
    }


    @Override
    public User clone() throws CloneNotSupportedException {
        User user = (User) super.clone();
        return user;
    }
}
  1. 再建立一個工作類
// 工作的實體類,並沒有實現Cloneable介面
public class Job {
    private String content;

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }
}
  1. 測試淺拷貝
/**
 * @author 一燈架構
 * @apiNote Java淺拷貝範例
 **/
public class Demo {

    public static void main(String[] args) throws CloneNotSupportedException {
        // 1. 建立使用者物件,{"name":"一燈架構","job":{"content":"開發"}}
        User user1 = new User();
        user1.setName("一燈架構");
        Job job1 = new Job();
        job1.setContent("開發");
        user1.setJob(job1);

        // 2. 拷貝使用者物件,name修改為"張三",工作內容修改"測試"
        User user2 = user1.clone();
        user2.setName("張三");
        Job job2 = user2.getJob();
        job2.setContent("測試");
        
        // 3. 輸出結果
        System.out.println("user原物件= " + user1);
        System.out.println("user拷貝物件= " + user2);
    }

}

輸出結果:

user原物件= {"name":"一燈架構","job":{"content":"測試"}}
user拷貝物件= {"name":"張三","job":{"content":"測試"}}

從結果中可以看出,物件拷貝把name修改為」張三「,原物件並沒有變,name是String型別,是基本資料型別,儲存在棧記憶體中。物件拷貝了一份新的棧記憶體資料,修改並不會影響原物件。

然後物件拷貝把Job中content修改為」測試「,原物件也跟著變了,原因是Job是參照型別,儲存在堆記憶體中。物件拷貝和原物件指向的同一個堆記憶體的地址,所以修改會影響到原物件。

3. 深拷貝的實現原理

深拷貝是既拷貝棧記憶體中的資料,又拷貝堆記憶體中的資料。

實現深拷貝有很多種方法,下面就詳細講解一下,看使用哪種方式更方便快捷。

3.1 實現Cloneable介面

通過實現Cloneable介面來實現深拷貝是最常見的。

想要實現clone功能,需要實現Cloneable介面,並重寫clone方法。

  1. 先建立一個使用者類
// 使用者的實體類,用作驗證
public class User implements Cloneable {
    private String name;
    
    // 每個使用者都有一個工作
    private Job job;

    public String getName() {
        return name;
    }

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

    public Job getJob() {
        return job;
    }

    public void setJob(Job job) {
        this.job = job;
    }


    @Override
    public User clone() throws CloneNotSupportedException {
        User user = (User) super.clone();
        // User物件中所有參照型別屬性都要執行clone方法
        user.setJob(user.getJob().clone());
        return user;
    }
}
  1. 再建立一個工作類
// 工作的實體類,需要實現Cloneable介面
public class Job implements Cloneable {
    private String content;

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }

    @Override
    protected Job clone() throws CloneNotSupportedException {
        return (Job) super.clone();
    }
}
  1. 測試淺拷貝
/**
 * @author 一燈架構
 * @apiNote Java深拷貝範例
 **/
public class Demo {

    public static void main(String[] args) throws CloneNotSupportedException {
        // 1. 建立使用者物件,{"name":"一燈架構","job":{"content":"開發"}}
        User user1 = new User();
        user1.setName("一燈架構");
        Job job1 = new Job();
        job1.setContent("開發");
        user1.setJob(job1);

        // 2. 拷貝使用者物件,name修改為"張三",工作內容修改"測試"
        User user2 = user1.clone();
        user2.setName("張三");
        Job job2 = user2.getJob();
        job2.setContent("測試");
        
        // 3. 輸出結果
        System.out.println("user原物件= " + user1);
        System.out.println("user拷貝物件= " + user2);
    }

}

輸出結果:

user原物件= {"name":"一燈架構","job":{"content":"開發"}}
user拷貝物件= {"name":"張三","job":{"content":"測試"}}

從結果中可以看出,user拷貝物件修改了name屬性和Job物件中內容,都沒有影響到原物件,實現了深拷貝。

通過實現Cloneable介面的方式來實現深拷貝,是Java中最常見的實現方式。

缺點是: 比較麻煩,需要所有實體類都實現Cloneable介面,並重寫clone方法。如果實體類中新增了一個參照物件型別的屬性,還需要新增到clone方法中。如果繼任者忘了修改clone方法,相當於挖了一個坑。

3.2 使用JSON字串轉換

實現方式就是:

  1. 先把user物件轉換成json字串
  2. 再把json字串轉換成user物件

這是個偏方,但是偏方治大病,使用起來非常方便,一行程式碼即可實現。

下面使用fastjson實現,使用Gson、Jackson也是一樣的:

import com.alibaba.fastjson.JSON;


/**
 * @author 一燈架構
 * @apiNote Java深拷貝範例
 **/
public class Demo {

    public static void main(String[] args) throws CloneNotSupportedException {
        // 1. 建立使用者物件,{"name":"一燈架構","job":{"content":"開發"}}
        User user1 = new User();
        user1.setName("一燈架構");
        Job job1 = new Job();
        job1.setContent("開發");
        user1.setJob(job1);

        //// 2. 拷貝使用者物件,name修改為"張三",工作內容修改"測試"
        User user2 = JSON.parseObject(JSON.toJSONString(user1), User.class);
        user2.setName("張三");
        Job job2 = user2.getJob();
        job2.setContent("測試");

        // 3. 輸出結果
        System.out.println("user原物件= " + JSON.toJSONString(user1));
        System.out.println("user拷貝物件= " + JSON.toJSONString(user2));
    }

}

輸出結果:

user原物件= {"name":"一燈架構","job":{"content":"開發"}}
user拷貝物件= {"name":"張三","job":{"content":"測試"}}

從結果中可以看出,user拷貝物件修改了name屬性和Job物件中內容,並沒有影響到原物件,實現了深拷貝。

3.3 集合實現深拷貝

再說一下Java集合怎麼實現深拷貝?

其實非常簡單,只需要初始化新物件的時候,把原物件傳入到新物件的構造方法中即可。

以最常用的ArrayList為例:

/**
 * @author 一燈架構
 * @apiNote Java深拷貝範例
 **/
public class Demo {

    public static void main(String[] args) throws CloneNotSupportedException {
        // 1. 建立原物件
        List<User> userList = new ArrayList<>();

        // 2. 建立深拷貝物件
        List<User> userCopyList = new ArrayList<>(userList);
    }

}

我是「一燈架構」,如果本文對你有幫助,歡迎各位小夥伴點贊、評論和關注,感謝各位老鐵,我們下期見