java 深克隆(深拷貝)與淺克隆(拷貝)詳解

来源:https://www.cnblogs.com/1314xf/archive/2018/12/18/10139971.html
-Advertisement-
Play Games

java深克隆和淺克隆 基本概念 1. 被覆制對象的所有變數都含有與原來的對象相同的值,而所有的對其他對象的引用仍然指向原來的對象。換言之,淺複製僅僅複製所拷貝的對象,而不複製它所引用的對象。 1. 被覆制對象的所有變數都含有與原來的對象相同的值,除去那些引用其他對象的變數。那些引用其他對象的變數將 ...


java深克隆和淺克隆

基本概念

  1. 淺複製(淺克隆)

被覆制對象的所有變數都含有與原來的對象相同的值,而所有的對其他對象的引用仍然指向原來的對象。換言之,淺複製僅僅複製所拷貝的對象,而不複製它所引用的對象。

  1. 深複製(深克隆)

    被覆制對象的所有變數都含有與原來的對象相同的值,除去那些引用其他對象的變數。那些引用其他對象的變數將指向被覆制過的新對象,而不再是原有的那些被引用的對象。換言之,深複製把要複製的對象所引用的對象都複製了一遍。

實現java深複製和淺複製的最關鍵的就是要實現Object中的clone()方法


如何使用clone()方法

首先我們來看一下Cloneable介面:

官方解釋:

1:實現此介面則可以使用java.lang.Object 的clone()方法,否則會拋出CloneNotSupportedException 異常

2:實現此介面的類應該使用公共方法覆蓋clone方法

3:此介面並不包含clone 方法,所以實現此介面並不能克隆對象,這隻是一個前提,還需覆蓋上面所講的clone方法。

public interface Cloneable {
}

看看Object裡面的Clone()方法:

  1. clone()方法返回的是Object類型,所以必須強制轉換得到克隆後的類型
  2. clone()方法是一個native方法,而native的效率遠遠高於非native方法,
  3. 可以發現clone方法被一個Protected修飾,所以可以知道必須繼承Object類才能使用,而Object類是所有類的基類,也就是說所有的類都可以使用clone方法
protected native Object clone() throws CloneNotSupportedException;

小試牛刀:

public class Person {
    public void testClone(){
        super.clone(); // 報錯了
    }
}

事實卻是clone()方法報錯了,那麼肯定奇怪了,既然Object是一切類的基類,並且clone的方法是Protected的,那應該是可以通過super.clone()方法去調用的,然而事實卻是會拋出CloneNotSupportedException異常, 官方解釋如下:

  1. 對象的類不支持Cloneable介面
  2. 覆蓋方法的子類也可以拋出此異常表示無法克隆實例。

所以我們更改代碼如下:

public class Person implements Cloneable{
    public void testClone(){
        try {
            super.clone();
            System.out.println("克隆成功");
        } catch (CloneNotSupportedException e) {
            System.out.println("克隆失敗");
            e.printStackTrace();
        }
    }
    public static void main(String[] args) {
        Person p = new Person();
        p.testClone();
    }
}

要註意,必須將克隆方法寫在try-catch塊中,因為clone方法會把異常拋出,當然程式也要求我們try-catch。


java.lang.object規範中對clone方法的約定

  1. 對任何的對象x,都有x.clone() !=x 因為克隆對象與原對象不是同一個對象
  2. 對任何的對象x,都有x.clone().getClass()= =x.getClass()//克隆對象與原對象的類型一樣
  3. 如果對象x的equals()方法定義恰當,那麼x.clone().equals(x)應該成立

對於以上三點要註意,這3項約定並沒有強制執行,所以如果用戶不遵循此約定,那麼將會構造出不正確的克隆對象,所以根據effective java的建議:

謹慎的使用clone方法,或者儘量避免使用。


淺複製實例

  1. 對象中全部是基本類型
public class Teacher implements Cloneable{
    private String name;
    private int age;

    public Teacher(String name, int age){
        this.name = name;
        this.age = age;
    }
    // 覆蓋
    @Override
    public Object clone() {
        try {
            return super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
            return null;
        }
    }
    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;
    }
}
// 客戶端測試
public class test {
    Teacher origin = new Teacher("tony", 11);
    System.out.println(origin.getName());
    Teacher clone = (Teacher) origin.clone();
    clone.setName("clone");
    System.out.println(origin.getName());
    System.out.println(clone.getName());
}

結果:

tony
tony
clone

1545111039834

從運行結果和圖上可以知道,克隆後的值變數會開闢新的記憶體地址,克隆對象修改值不會影響原來對象。

  1. 對象中含有引用類型
public class Teacher implements Cloneable{
    private String name;
    private int age;
    private Student student;

    public Teacher(String name, int age, Student student){
        this.name = name;
        this.age = age;
        this.student = student;
    }
    // 覆蓋
    @Override
    public Object clone() {
        try {
            return super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
            return null;
        }
    }
    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;
    }

    public Student getStudent() {
        return student;
    }

    public void setStudent(Student student) {
        this.student = student;
    }
}

// 學生類
public class Student {
    private String name;
    private int age;
    public Student(String name, int age){
        this.name = name;
        this.age = age;
    }
     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;
    }
}
// 客戶端測試
public class test {
    public static void main(String[] args) {
        Student student = new Student("學生1" ,11);
        Teacher origin = new Teacher("老師", 11, student);;
        Teacher clone = (Teacher) origin.clone();
        System.out.println("比較克隆後的引用對象");
        System.out.println(origin.getStudent().getClass() == clone.getStudent().getClass());
        Student student2 = new Student("學生2", 12);
        clone.setStudent(student2);
        System.out.println("克隆後,比較克隆對象改變引用");
        System.out.println(origin.getStudent().getClass() == clone.getStudent().getClass());
    }
}

運行結果:

比較克隆後的引用對象
true
克隆後,比較克隆對象改變引用
true

1545109406152

如圖可知,引用類型只會存在一份記憶體地址,執行object的clone方法拷貝的也是引用的複製(這部分的記憶體空間不一樣,)但是引用指向的記憶體空間是一樣的,原對象修改引用變數或者淺拷貝對象修改引用變數都會引起雙方的變化

重點:綜上兩個方面可以知道,Object的clone方法是屬於淺拷貝,基本變數類型會複製相同值,而引用變數類型也是會複製相同的引用。


深複製實例

從上面的淺拷貝可以知道,對於引用的變數只會拷貝引用指向的地址,也就是指向同一個記憶體地址,但是很多情況下我們需要的是下麵圖的效果:

1545121868853

深拷貝實現的是對所有可變(沒有被final修飾的引用變數)引用類型的成員變數都開闢記憶體空間所以一般深拷貝對於淺拷貝來說是比較耗費時間和記憶體開銷的。

深拷貝的兩種方式:

  1. 重寫clone方法實現深拷貝

學生類:

public class Student implements Cloneable {
    private String name;
    private int age;
    public Student(String name, int age){
        this.name = name;
        this.age = age;
    }

    @Override
    protected Object clone()  {
        try {
            return super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
            return null;
        }
    }

    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;
    }
}

老師類:

public class Teacher implements Cloneable{
    private String name;
    private int age;
    private Student student;

    public Teacher(String name, int age, Student student){
        this.name = name;
        this.age = age;
        this.student = student;
    }
    // 覆蓋
    @Override
    public Object clone() {
        Teacher t = null;
        try {
            t = (Teacher) super.clone();
            t.student = (Student)student.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return t;
    }
    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;
    }

    public Student getStudent() {
        return student;
    }

    public void setStudent(Student student) {
        this.student = student;
    }
}

測試端:

public class test {
    public static void main(String[] args) {
        Student s = new Student("學生1", 11);
        Teacher origin = new Teacher("老師原對象", 23, s);
        System.out.println("克隆前的學生姓名:" + origin.getStudent().getName());
        Teacher clone = (Teacher) origin.clone();
        // 更改克隆後的學生信息 更改了姓名
        clone.getStudent().setName("我是克隆對象更改後的學生2");
        System.out.println("克隆後的學生姓名:" + clone.getStudent().getName());
    }
}

運行結果:

克隆前的學生姓名:學生1
克隆後的學生姓名:我是克隆對象更改後的學生2

  1. 序列化實現深克隆

我們發現上面通過object的clone方法去實現深克隆十分麻煩, 因此引出了另外一種方式:序列化實現深克隆

概念:

  • 序列化:把對象寫到流里
  • 反序列化:把對象從流中讀出來

在Java語言里深複製一個對象,常常可以先使對象實現Serializable介面,然後把對象(實際上只是對象的一個拷貝)寫到一個流里,再從流里讀出來,便可以重建對象。

註意:

  • 寫在流里的是對象的一個拷貝,而原對象仍然存在於JVM裡面
  • 對象以及對象內部所有引用到的對象都是可序列化的
  • 如果不想序列化,則需要使用transient來修飾

案例:

Teacher:

public class Teacher implements Serializable{
    private String name;
    private int age;
    private Student student;

    public Teacher(String name, int age, Student student){
        this.name = name;
        this.age = age;
        this.student = student;
    }
    // 深克隆
    public Object deepClone() throws IOException, ClassNotFoundException {
        // 序列化
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(bos);
        oos.writeObject(this);
        // 反序列化
        ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
        ObjectInputStream ois = new ObjectInputStream(bis);
        return ois.readObject();
    }
    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;
    }

    public Student getStudent() {
        return student;
    }

    public void setStudent(Student student) {
        this.student = student;
    }

Student:

public class Student implements Serializable {
    private String name;
    private int age;
    public Student(String name, int age){
        this.name = name;
        this.age = age;
    }

    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;
    }
}

client:

public class test {
    public static void main(String[] args) {
        try {
            Student s = new Student("學生1", 11);
            Teacher origin = new Teacher("老師原對象", 23, s);
            System.out.println("克隆前的學生姓名:" + origin.getStudent().getName());
            Teacher clone = (Teacher) origin.deepClone();
            // 更改克隆後的d學生信息 更改了姓名
            clone.getStudent().setName("我是克隆對象更改後的學生2");
            System.out.println("克隆後的學生姓名:" + clone.getStudent().getName());
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

當然這些工作都有現成的輪子了,藉助於Apache Commons可以直接實現:

  • 淺克隆:BeanUtils.cloneBean(Object obj);
  • 深克隆:SerializationUtils.clone(T object);

最後探討

  • 在java中為什麼實現了Cloneable介面,就可以調用Object中的Clone方法

    參考以下回答:

https://www.zhihu.com/question/52490586


號外號外:

  • 如果有小伙伴覺得我寫的不錯的話可以關註一下我的博客哦
  • 可以關註下方我的公眾號java架構師小密圈,回覆1獲取2Tjava架構師必備乾貨另外:小伙伴可以回覆任意想學的技術,可以免費幫你搜尋其實我們還需要學很多!!!!!!

1545271756941

  • 還會分享一些賺錢理財的小套路哦,歡迎大家來支持,一起學習成長,程式員不僅僅是搬瓦工!

公眾號:分享系列好文章
java架構師小密圈

交流群:一起奔著java架構師努力
java架構師小密圈


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 所有基礎課程鏈接: 1.JavaScript基礎視頻教程總結(001-010章) 2.JavaScript基礎視頻教程總結(011-020章) 3. JavaScript基礎視頻教程總結(021-030章) 4. JavaScript基礎視頻教程總結(031-040章) 5. JavaScript基 ...
  • 1.什麼是作用域(scope)? 簡單來講,作用域(scope)就是變數訪問規則的有效範圍。 作用域外,無法引用作用域內的變數; 離開作用域後,作用域的變數的記憶體空間會被清除,比如執行完函數或者關閉瀏覽器 作用域與執行上下文是完全不同的兩個概念。我曾經也混淆過他們,但是一定要仔細區分。 JavaSc ...
  • 今天在寫一個小的 CSS Demo,一個關於 3d 球的旋轉動畫,關於 CSS 3D,少不了會使用下麵這幾個屬性: 這個 Demo 你可以戳這裡,大概是這樣:CodePen Demo - 3D ball: 嗯,大概到了這個效果,想到了 CSS 混合模式 mix-blend-mode,尋思著,利用混合 ...
  • 使用create react app腳手架搭建環境 1、安裝node 。軟體下載地址:,我下的推薦的版本。 安裝之後測試是否安裝成功。windows系統下,輸入Win+R輸入cmd ,打開運行視窗,輸入 node v 顯示版本號則表示安裝成功。接下來可以看下是否安裝npm,輸入npm v ,顯示版本 ...
  • 首先給一個神奇的圖: 我的反應,精分吧!一會兒true一會兒false的。。。 後來發現,把g去掉後就正常了,那這是為什麼呢??lastIndex惹得鬼! 正文: lastIndex 全局正則表達是,有一個屬性:lastIndex,這個屬性是用來存放上一次匹配文本之後的第一個字元的位置。 exec( ...
  • 通過 jQuery,可以很容易地添加新元素/內容。 添加新的 HTML 內容 一共有四種方法: append() - 在被選元素的結尾插入內容 prepend() - 在被選元素的開頭插入內容 after() - 在被選元素之後插入內容 before() - 在被選元素之前插入內容 以下為jQuer ...
  • 閉包在紅寶書中的解釋就是:有權訪問另一個函數作用域中的變數的函數。 1.變數作用域 全局變數:所有的函數外部定義的變數,它的作用域是整個script。 局部變數:定義在函數體內部的變數,作用域僅限於函數體內部。離開函數體就會無效。再調用就是出錯。 舉例如下-局部變數: a變數定義在fun函數內,是局 ...
  • 什麼是瀏覽器內核 瀏覽器內核的分類 瀏覽器內核的作用 W3C 帶著問題去瞭解 1、什麼是瀏覽器內核? 瀏覽器端內核就是開發商按照約定好的規則開發出的能夠將正確開發者代碼渲染成頁面的東西 2、瀏覽器內核的分類: webkit內核:chrome瀏覽器及大部分安卓手機、safari Gecko內核:FF ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...