1. 程式人生 > >Java 淺拷貝、深拷貝,你知多少?

Java 淺拷貝、深拷貝,你知多少?

這是今天我們在技術群裡面討論的一個知識點,討論的相當激烈,由於對這一塊使用的比較少,所以對這一塊多少有些盲區。這篇文章總結了所討論的內容,希望這篇文章對你有所幫助。

在 Java 開發中,物件拷貝或者說物件克隆是常有的事,物件克隆最終都離不開直接賦值、淺拷貝、深拷貝 這三種方式,其中直接賦值應該是我們最常用的一種方式吧,對於淺拷貝和深拷貝可能用的少,所以或多或少存在一些誤區,這篇文章會詳細的介紹這三種物件克隆方式。

前置知識
值型別:Java 的基本資料型別,例如 int、float 
引用型別:自定義類和 Java 包裝類(string、integer)

直接賦值

直接賦值是我們最常用的方式,在我們程式碼中的體現是Persona = new Person();Person b = a

,是一種簡單明瞭的方式,但是它只是拷貝了物件引用地址而已,並沒有在記憶體中生成新的物件,我們可以通過下面這個例子來證明這一點

// person 物件
public class Person {
    // 姓名
    private String name;
    // 年齡
    private int age;
    // 郵件
    private String email;
    // 描述
    private String desc;
    ...省略get/set...
 }
 // main 方法
public class PersonApp {
   public static void main(String[] args) {
       // 初始化一個物件
       Person person = new Person("張三",20,"[email protected]","我是張三");
       // 複製物件
       Person person1 = person;
       // 改變 person1 的屬性值
       person1.setName("我不是張三了");
        System.out.println("person物件:"+person);
        System.out.println("person1物件:"+person1);

   }
}

執行上面程式碼,你會得到如下結果:

person物件:Person{name='我不是張三了', age=20, email='[email protected]', desc='我是張三'}
person1物件:Person{name='我不是張三了', age=20, email='[email protected]', desc='我是張三'}

我們將 person 物件複製給了 person1 物件,我們對 person1 物件的 name 屬性進行了修改,並未修改 person 物件的name 屬性值,但是我們最後發現 person 物件的 name 屬性也發生了變化,其實不止這一個值,對於其他值也是一樣的,所以這結果證明了我們上面的結論:直接賦值的方式沒有生產新的物件,只是生新增了一個物件引用,直接賦值在 Java 記憶體中的模型大概是這樣的

淺拷貝

淺拷貝也可以實現物件克隆,從這名字你或許可以知道,這種拷貝一定存在某種缺陷,是的,它就是存在一定的缺陷,先來看看淺拷貝的定義:如果原型物件的成員變數是值型別,將複製一份給克隆物件,也就是說在堆中擁有獨立的空間;如果原型物件的成員變數是引用型別,則將引用物件的地址複製一份給克隆物件,也就是說原型物件和克隆物件的成員變數指向相同的記憶體地址。換句話說,在淺克隆中,當物件被複制時只複製它本身和其中包含的值型別的成員變數,而引用型別的成員物件並沒有複製。 可能你沒太理解這段話,那麼我們在來看看淺拷貝的通用模型:

要實現物件淺拷貝還是比較簡單的,只需要被複制類需要實現 Cloneable 介面,重寫 clone 方法即可,對 person 類進行改造,使其可以支援淺拷貝。

public class Person implements Cloneable {
    // 姓名
    private String name;
    // 年齡
    private int age;
    // 郵件
    private String email;
    // 描述
    private String desc;

    /*
    * 重寫 clone 方法,需要將許可權改成 public ,直接呼叫父類的 clone 方法就好了
    */
    @Override
    public Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
    ...省略...
}

改造很簡單隻需要讓 person 繼承 Cloneable 介面,並且重寫 clone 方法即可,clone 也非常簡單隻需要呼叫 object 的 clone 方法就好,唯一需要注意的地方就是 clone 方法需要用 public 來修飾,在簡單的修改 main 方法

public class PersonApp {
    public static void main(String[] args) throws Exception {
        // 初始化一個物件
        Person person = new Person("張三",20,"[email protected]","我是張三");
        // 複製物件
        Person person1 = (Person) person.clone();
        // 改變 person1 的屬性值
        person1.setName("我是張三的克隆物件");
        // 修改 person age 的值
        person1.setAge(22);
        System.out.println("person物件:"+person);
        System.out.println();
        System.out.println("person1物件:"+person1);

    }
}

重新執行 main 方法,結果如下:

person物件:Person{name='張三', age=20, email='[email protected]', desc='我是張三'}

person1物件:Person{name='我是張三的克隆物件', age=22, email='[email protected]', desc='我是張三'}

看到這個結果,你是否有所質疑呢?說好的引用物件只是拷貝了地址,為啥修改了 person1 物件的 name 屬性值,person 物件沒有改變?這裡就是一個非常重要的知識點了,,原因在於:String、Integer 等包裝類都是不可變的物件,當需要修改不可變物件的值時,需要在記憶體中生成一個新的物件來存放新的值,然後將原來的引用指向新的地址,所以在這裡我們修改了 person1 物件的 name 屬性值,person1 物件的 name 欄位指向了記憶體中新的 name 物件,但是我們並沒有改變 person 物件的 name 欄位的指向,所以 person 物件的 name 還是指向記憶體中原來的 name 地址,也就沒有變化

這種引用是一種特列,因為這些引用具有不可變性,並不具備通用性,所以我們就自定義一個類,來演示淺拷貝,我們定義一個 PersonDesc 類用來存放person 物件中的 desc 欄位,,然後在 person 物件中引用 PersonDesc 類,具體程式碼如下:

// 新增 PersonDesc 
public class PersonDesc {
    // 描述
    private String desc;

}
public class Person implements Cloneable {
    // 姓名
    private String name;
    // 年齡
    private int age;
    // 郵件
    private String email;
    // 將原來的 string desc 變成了 PersonDesc 物件,這樣 personDesc 就是引用型別
    private PersonDesc personDesc;

    @Override
    public Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
    public void setDesc(String desc) {
        this.personDesc.setDesc(desc);
    }
    public Person(String name, int age, String email, String desc) {
        this.name = name;
        this.age = age;
        this.email = email;
        this.personDesc = new PersonDesc();
        this.personDesc.setDesc(desc);
    }
     ...省略...
}

修改 main 方法

public class PersonApp {
    public static void main(String[] args) throws Exception {
        // 初始化一個物件
        Person person = new Person("平頭哥",20,"[email protected]","我的公眾號是:平頭哥的技術博文");
        // 複製物件
        Person person1 = (Person) person.clone();
        // 改變 person1 的屬性值
        person1.setName("我是平頭哥的克隆物件");
        // 修改 person age 的值
        person1.setAge(22);
        person1.setDesc("我已經關注了平頭哥的技術博文公眾號");
        System.out.println("person物件:"+person);
        System.out.println();
        System.out.println("person1物件:"+person1);
    }
}

執行 main 方法,得到如下結果:

person物件:Person{name='平頭哥', age=20, email='[email protected]', desc='我已經關注了平頭哥的技術博文公眾號'}

person1物件:Person{name='我是平頭哥的克隆物件', age=22, email='[email protected]', desc='我已經關注了平頭哥的技術博文公眾號'}

我們修改 person1 的 desc 欄位之後,person 的 desc 也發生了改變,這說明 person 物件和 person1 物件指向是同一個 PersonDesc 物件地址,這也符合淺拷貝引用物件只拷貝引用地址並未建立新物件的定義,到這你應該知道淺拷貝了吧。

深拷貝

深拷貝也是物件克隆的一種方式,相對於淺拷貝,深拷貝是一種完全拷貝,無論是值型別還是引用型別都會完完全全的拷貝一份,在記憶體中生成一個新的物件,簡單點說就是拷貝物件和被拷貝物件沒有任何關係,互不影響。深拷貝的通用模型如下:

深拷貝有兩種方式,一種是跟淺拷貝一樣實現 Cloneable 介面,另一種是實現 Serializable 介面,用序列化的方式來實現深拷貝,我們分別用這兩種方式來實現深拷貝

實現 Cloneable 介面方式

實現 Cloneable 介面的方式跟淺拷貝相差不大,我們需要引用物件也實現 Cloneable 介面,具體程式碼改造如下:

public class PersonDesc implements Cloneable{

    // 描述
    private String desc;
    ...省略...
    @Override
    public Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}
public class Person implements Cloneable {
    // 姓名
    private String name;
    // 年齡
    private int age;
    // 郵件
    private String email;

    private PersonDesc personDesc;

    /**
    * clone 方法不是簡單的呼叫super的clone 就好,
    */
    @Override
    public Object clone() throws CloneNotSupportedException {
        Person person = (Person)super.clone();
        // 需要將引用物件也克隆一次
        person.personDesc = (PersonDesc) personDesc.clone();
        return person;
    }
    ...省略...
}

main 方法不需要任何改動,我們再次執行 main 方法,得到如下結果:

person物件:Person{name='平頭哥', age=20, email='[email protected]', desc='我的公眾號是:平頭哥的技術博文'}

person1物件:Person{name='我是平頭哥的克隆物件', age=22, email='[email protected]', desc='我已經關注了平頭哥的技術博文公眾號'}

可以看出,修改 person1 的 desc 時對 person 的 desc 已經沒有影響了,說明進行了深拷貝,在記憶體中重新生成了一個新的物件。

實現 Serializable 介面方式

實現 Serializable 介面方式也可以實現深拷貝,而且這種方式還可以解決多層克隆的問題,多層克隆就是引用型別裡面又有引用型別,層層巢狀下去,用 Cloneable 方式實現還是比較麻煩的,一不小心寫錯了就不能實現深拷貝了,使用 Serializable 序列化的方式就需要所有的物件對實現 Serializable 介面,我們對程式碼進行改造,改造成序列化的方式

public class Person implements Serializable {

    private static final long serialVersionUID = 369285298572941L;
    // 姓名
    private String name;
    // 年齡
    private int age;
    // 郵件
    private String email;

    private PersonDesc personDesc;

    public Person clone() {
        Person person = null;
        try { // 將該物件序列化成流,因為寫在流裡的是物件的一個拷貝,而原物件仍然存在於JVM裡面。所以利用這個特性可以實現物件的深拷貝
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            ObjectOutputStream oos = new ObjectOutputStream(baos);
            oos.writeObject(this);
            // 將流序列化成物件
            ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
            ObjectInputStream ois = new ObjectInputStream(bais);
            person = (Person) ois.readObject();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        return person;
    }

    public void setDesc(String desc) {
        this.personDesc.setDesc(desc);
    }
  ...省略...
}
public class PersonDesc implements Serializable {

    private static final long serialVersionUID = 872390113109L; 
    // 描述
    private String desc;

    public String getDesc() {
        return desc;
    }

    public void setDesc(String desc) {
        this.desc = desc;
    }

}
public class PersonApp {
    public static void main(String[] args) throws Exception {
        // 初始化一個物件
        Person person = new Person("平頭哥",20,"[email protected]","我的公眾號是:平頭哥的技術博文");
        // 複製物件
        Person person1 = (Person) person.clone();
        // 改變 person1 的屬性值
        person1.setName("我是平頭哥的克隆物件");
        // 修改 person age 的值
        person1.setAge(22);
        person1.setDesc("我已經關注了平頭哥的技術博文公眾號");
        System.out.println("person物件:"+person);
        System.out.println();
        System.out.println("person1物件:"+person1);
    }
}

執行 main 方法,我們可以得到跟 Cloneable 方式一樣的結果,序列化的方式也實現了深拷貝。到此關於 Java 淺拷貝和深拷貝的相關內容就介紹完了,希望你有所收穫。

最後

目前網際網路上很多大佬都有 Java 物件克隆文章,如有雷同,請多多包涵了。原創不易,碼字不易,還希望大家多多支援。若文中有所錯誤之處,還望提出,謝謝。

歡迎掃碼關注微信公眾號:「平頭哥的技術博文」,和平頭哥一起學習,一起進步。

相關推薦

Java 拷貝拷貝多少?

這是今天我們在技術群裡面討論的一個知識點,討論的相當激烈,由於對這一塊使用的比較少,所以對這一塊多少有些盲區。這篇文章總結了所討論的內容,希望這篇文章對你有所幫助。 在 Java 開發中,物件拷貝或者說物件克隆是常有的事,物件克隆最終都離不開直接賦值、淺拷貝、深拷貝 這三種方式,其中直接賦值應該是我們最常

一文搞懂Java引用拷貝拷貝拷貝

>微信搜一搜 **「bigsai」** 專注於Java和資料結構與演算法的鐵鐵 >文章收錄在[github/bigsai-algorithm](https://github.com/javasmall/bigsai-algorithm) 在開發、刷題、面試中,我們可能會遇到將一個物件的屬性

=拷貝拷貝小結

使用 deepcopy spa 不同 logs 內存 color 但是 引用 >>> import copy // = 指向同一個內存空間,操作的是同一個對象 >>> t1 = {‘a‘:1, ‘b‘:2} >>>

PHP拷貝拷貝簡析

php 淺拷貝 深拷貝 clone 克隆 前言: 在PHP中, “=” 作為賦值符號,對於普通變量是深拷貝,對於對象來說是淺拷貝(對象的賦值是引用賦值)。 註意:對象作為參數傳遞時,也是引用傳遞,無論函數定義時參數前面是否有&符號。簡述:

Python賦值拷貝拷貝的區別

ID IV 元素 改變 拷貝 但是 int 需要 copy 一、賦值 str例子 >>> a = 'hello' >>> b = 'hello' >>> c = a >>&

拷貝拷貝的區別

沒有 動手 包含 import 方式 工廠 highlight 拷貝 華麗 一,淺拷貝:創建一個新的對象,但它包含的是對原始對象中包含項的引用(如果用引用的方式修改 其中一個對象,另外一個也會修改改變){1,完全切片方法;2,工廠函數,如 list();3,copy 模塊

python 字典賦值拷貝拷貝

# 專案開發中,因字典可修改,所以注意拷貝方式 import copy   1. 原字典操作 dict1 = {'user': 'test', 'num': [1, 2, 3]}              &

python學習(六):python中賦值拷貝拷貝的區別

存在賦值、淺拷貝、深拷貝問題的資料型別是對組合物件來說,所謂的組合物件就是包含了其它物件的物件,如列表,類例項。 其他的單個物件則不存在這個問題。 可變物件: list, dict. 不可變物件有: int, string, float, tuple.  

python閒談----關於Python中列表的賦值與拷貝(拷貝拷貝)操作

列表作為python中最重要也是最基礎的序列,以其簡單方便的操作被廣泛應用,但在引用列表時也要注意一些小坑,這裡討論一下關於列表賦值的操作。 《Fluent Python》的作者Luciano Ramalho將序列按照記憶體模型分為容器序列和扁平序列

python 拷貝拷貝拷貝之間的聯絡

import copy #淺拷貝與深拷貝 #*************************************************** #*************************************************** #**

python賦值拷貝拷貝區別

閱讀目錄 在寫Python過程中,經常會遇到物件的拷貝,如果不理解淺拷貝和深拷貝的概念,你的程式碼就可能出現一些問題。所以,在這裡按個人的理解談談它們之間的區別。   回到頂部 一、賦值(assignment) 在《Python FAQ1》一文中,對賦值已經講的很清楚

Python中賦值拷貝拷貝的區別?

1.賦值(=),就是建立了物件的一個新的引用,修改其中任意一個變數都會影響到另一個。 2.淺拷貝:建立一個新的物件,但它包含的是對原始物件中包含項的引用(如果引用的方式修改其中的一個物件,另外一個也會修改改變) {1.完全切片方法;2.工廠函式,如list();3.copy模組的cop

javascript---物件和函式的引用拷貝拷貝遞迴

1、javascript 對象和函式的引用 <!doctype html> <html lang="en"> <head> <meta charset="UTF-8"> <title>javascript 物

Python:賦值拷貝拷貝

賦值:a=b,只拷貝了物件的引用,沒有拷貝內容。兩個物件的id一樣 淺拷貝:copy.copy(),構造一個新的複合物件,然後將子物件引用插入到原始物件的副本中。 深拷貝:copy.deepcopy(),構造一個新的複合物件,然後遞迴地將子物件副本插入原始物件的副本。 給個栗子:

拷貝拷貝賦值賦值

一、淺拷貝     物件初始化物件的時候調動拷貝建構函式,只是拷貝指標指向的拷貝構造稱為淺拷貝。       當要析勾的時候物件被一一析勾的時候,第二個析勾的物件就找不到需要釋放的空間,程式報錯。

賦值拷貝拷貝的理解

(一)2個為什麼           先通過2個為什麼來了解一下python記憶體中變數的儲存情況。   >>> name = [1,2,3,["alex","rain"

【C++】C++拷貝拷貝及引用計數淺析

在C++開發中,經常遇到的一個問題就是與指標相關的記憶體管理問題,稍有不慎,就會造成記憶體洩露、記憶體破壞等嚴重的問題。不像Java一樣,沒有指標這個概念,所以也就不必擔心與指標相關的一系列問題,但C++不同,從C語言沿襲下來的指標是其一大特點,我們常常要使用n

【C++】c++String類拷貝拷貝

在瞭解深拷貝以及寫時拷貝之前,我們先來了解什麼是淺拷貝,看下面程式碼: class String { public: String(char* ptr = "") :_ptr(new char[strlen(ptr)+1])

Python 賦值拷貝拷貝的區別?

http://songlee24.github.io/2014/08/15/python-FAQ-02/ 在寫Python過程中,經常會遇到物件的拷貝,如果不理解淺拷貝和深拷貝的概念,你的程式碼就可能出現一些問題。所以,在這裡按個人的理解談談它們之間的區別。 一、賦

拷貝拷貝

深拷貝和淺拷貝   這兩個概念是在專案中比較常見的,在很多時候,都會遇到拷貝的問題,我們總是需要將一個物件賦值到另一個物件上,但可能會在改變新賦值物件的時候,忽略掉我是否之後還需要用到原來的物件,那麼就會出現當改變新賦值物件的某一個屬性時,也同時改變了原物件,此時我們就需要用到拷貝這個概念了。