新四季網

java什麼是深拷貝和淺拷貝(輕鬆理解深拷貝與淺拷貝)

2023-10-06 07:00:49 2

作者 | aduner

來源 | urlify.cn/yuyYra

前言

本文代碼中有用到一些註解,主要是Lombok與junit用於簡化代碼。

主要是看到一堆代碼會很亂,這樣理解更清晰。如果沒用過不用太過糾結。

對象的拷貝(克隆)是一個非常高頻的操作,主要有以下三種方式:

直接賦值拷貝:淺拷貝深拷貝

因為Java沒有指針的概念,或者說是不需要我們去操心,這讓我們省去了很多麻煩,但相應的,對於對象的引用、拷貝有時候就會有些懵逼,藏下一些很難發現的bug。

為了避免這些bug,理解這三種操作的作用與區別就是關鍵。

直接賦值

用等於號直接賦值是我們平時最常用的一種方式。

它的特點就是直接引用等號右邊的對象

先來看下面的例子

先創建一個Person類

@Data@AllArgsConstructor @ToStringpublic class Person{ private String name; private int age; private Person friend;}

測試

@Testpublic void test { Person friend =new Person("老王",30,null); Person person1 = new Person("張三", 20, null); Person person2 = person1; System.out.println("person1: " person1); System.out.println("person2: " person2 "\n"); person1.setName("張四"); person1.setAge(25); person1.setFriend(friend); System.out.println("person1: " person1); System.out.println("person2: " person2);}

結果

person1: Person(name=張三, age=20, friend=null)person2: Person(name=張三, age=20, friend=null)person1: Person(name=張四, age=25, friend=Person(name=老王, age=30, friend=null))person2: Person(name=張四, age=25, friend=Person(name=老王, age=30, friend=null))

分析:

可以看到通過直接賦值進行拷貝,其實就只是單純的對前對象進行引用。

如果這些對象都是基礎對象當然沒什麼問題,但是如果對象進行操作,相當於兩個對象同屬一個實例。

拷貝

直接賦值雖然方便,但是很多時候並不是我們想要的結果,很多時候我們需要的是兩個看似一樣但是完全獨立的兩個對象。

這種時候我們就需要用到一個方法clone

clone並不是一個可以直接使用的方法,需要先實現Cloneable接口,然後重寫它才能使用。

protected native Object clone throws CloneNotSupportedException;

clone方法被native關鍵字修飾,native關鍵字說明其修飾的方法是一個原生態方法,方法對應的實現不是在當前文件,而是系統或者其他語言來實現。

淺拷貝

淺拷貝可以實現對象克隆,但是存在一些缺陷。

定義:

如果原型對象的成員變量是值類型,將複製一份給克隆對象,也就是在堆中擁有獨立的空間;如果原型對象的成員變量是引用類型,則將引用對象的地址複製一份給克隆對象,指向相同的內存地址。舉例

光看定義不太好一下子理解,上代碼看例子。

我們先來修改一下Person類,實現Cloneable接口,重寫clone方法,其實很簡單,只需要用super調用一下即可

@Data@AllArgsConstructor@ToStringpublic class Person implements Cloneable { private String name; private int age; private Friend friend; @Override public Object clone { try { return super.clone; } catch (CloneNotSupportedException e) { e.printStackTrace; } return null; }}------- @Data@AllArgsConstructorpublic class Friend { private String Name;}

測試

@Testpublic void test { Person person1 = new Person("張三", 20, "老王"); Person person2 = (Person) person1.clone; System.out.println("person1: " person1); System.out.println("person2: " person2 "\n"); person1.setName("張四"); person1.setAge(25); person1.setFriend("小王"); System.out.println("person1: " person1); System.out.println("person2: " person2);}

結果

person1: Person(name=張三, age=20, friend=Friend(Name=老王))person2: Person(name=張三, age=20, friend=Friend(Name=老王))person1: Person(name=張四, age=25, friend=Friend(Name=小王))person2: Person(name=張三, age=20, friend=Friend(Name=小王))

可以看到,name age基本對象屬性並沒改變,而friend引用對象熟悉變了。

原理

Java淺拷貝的原理其實是把原對象的各個屬性的地址拷貝給新對象。

注意我說的是各個屬性,就算是基礎對象屬性其實也是拷貝的地址。

你可能有點暈了,都是拷貝了地址,為什麼修改了 person1 對象的 name age 屬性值,person2 對象的 name age 屬性值沒有改變呢?

我們一步步來,拿name屬性來說明:

String、Integer 等包裝類都是不可變的對象當需要修改不可變對象的值時,需要在內存中生成一個新的對象來存放新的值然後將原來的引用指向新的地址我們修改了 person1 對象的 name 屬性值,person1 對象的 name 欄位指向了內存中新的 String 對象我們並沒有改變 person2 對象的 name 欄位的指向,所以 person2 對象的 name 還是指向內存中原來的 String 地址

看圖

這個圖已經很清晰的展示了其中的過程,因為person1 對象改變friend時是改變的引用對象的屬性,並不是新建立了一個對象進行替換,原本老王的消失了,變成了小王。所以person2也跟著改變了。

深拷貝

深拷貝就是我們拷貝的初衷了,無論是值類型還是引用類型都會完完全全的拷貝一份,在內存中生成一個新的對象。

拷貝對象和被拷貝對象沒有任何關係,互不影響。

深拷貝相比於淺拷貝速度較慢並且花銷較大。

簡而言之,深拷貝把要複製的對象所引用的對象都複製了一遍。

因為Java本身的特性,對於不可變的基本值類型,無論如何在內存中都是只有一份的。

所以對於不可變的基本值類型,深拷貝跟淺拷貝一樣,不過並不影響什麼。

實現:

想要實現深拷貝並不難,只需要在淺拷貝的基礎上進行一點修改即可。

給friend添加一個clone方法。在Person類的clone方法調用friend的clone方法,將friend也複製一份即可。

@Data@ToStringpublic class Person implements Cloneable { private String name; private int age; private Friend friend; public Person(String name, int age, String friend) { this.name = name; this.age = age; this.friend = new Friend(friend); } public void setFriend(String friend) { this.friend.setName(friend); } @Override public Object clone { try { Person person = (Person)super.clone; person.friend = (Friend) friend.clone; return person; } catch (CloneNotSupportedException e) { e.printStackTrace; } return null; }}------ @Data@AllArgsConstructorpublic class Friend implements Cloneable{ private String Name; @Override public Object clone throws CloneNotSupportedException { return super.clone; }}

測試

@Testpublic void test {Person person1 = new Person("張三", 20, "老王");Person person2 = (Person) person1.clone;System.out.println("person1: " person1);System.out.println("person2: " person2 "\n");person1.setName("張四");person1.setAge(25);person1.setFriend("小王");System.out.println("person1: " person1);System.out.println("person2: " person2);}

結果

person1: Person(name=張三, age=20, friend=Friend(Name=老王))person2: Person(name=張三, age=20, friend=Friend(Name=老王))person1: Person(name=張四, age=25, friend=Friend(Name=小王))person2: Person(name=張三, age=20, friend=Friend(Name=老王))

分析:

可以看到這次是真正的完全獨立了起來。

需要注意的是,如果Friend類本身也存在引用類型,則需要在Friend類中的clone,也去調用其引用類型的clone方法,就如是Person類中那樣,對!就是套娃!

所以對於存在多層依賴關係的對象,實現Cloneable接口重寫clone方法就顯得有些笨拙了。

這裡我們在介紹一種方法:利用序列化實現深拷貝

Serializable 實現深拷貝

修改Person和Friend,實現Serializable接口

@Data@ToStringpublic class Person implements Serializable { // ......同之前 public Object deepClone throws Exception { // 序列化 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; }}--- @Data@AllArgsConstructorpublic class Friend implements Serializable { private String Name;}

測試

@Testpublic void test { Person person1 = new Person("張三", 20, "老王"); Person person2 = null; try { person2 = (Person) person1.deepClone; } catch (Exception e) { e.printStackTrace; } System.out.println("person1: " person1); System.out.println("person2: " person2 "\n"); person1.setName("張四"); person1.setAge(25); person1.setFriend("小王"); System.out.println("person1: " person1); System.out.println("person2: " person2);}

結果

person1: Person(name=張三, age=20, friend=Friend(Name=老王))person2: Person(name=張三, age=20, friend=Friend(Name=老王))person1: Person(name=張四, age=25, friend=Friend(Name=小王))person2: Person(name=張三, age=20, friend=Friend(Name=老王))

只要將會被複製到的引用對象標記Serializable接口,通過序列化到方式即可實現深拷貝。

原理:

對象被序列化成流後,因為寫在流裡的是對象的一個拷貝,而原對象仍然存在於虛擬機裡面。

通過反序列化就可以獲得一個完全相同的拷貝。

利用這個特性就實現了對象的深拷貝。

總結直接賦值是將新的對象指向原對象所指向的實例,所以一旦有所修改,兩個對象會一起變。淺拷貝是把原對象屬性的地址傳給新對象,對於不可變的基礎類型,實現了二者的分離,但對於引用對象,二者還是會一起改變。深拷貝是真正的完全拷貝,二者沒有關係。實現深拷貝時如果存在多層依賴關係,可以採用序列化的方式來進行實現。

對於Serializable接口、Cloneable接口,其實都是相當於一個標記,點進去看源碼,其實他們是一個空接口。

,
同类文章
葬禮的夢想

葬禮的夢想

夢見葬禮,我得到了這個夢想,五個要素的五個要素,水火只好,主要名字在外面,職業生涯良好,一切都應該對待他人治療誠意,由於小,吉利的冬天夢想,秋天的夢是不吉利的
找到手機是什麼意思?

找到手機是什麼意思?

找到手機是什麼意思?五次選舉的五個要素是兩名士兵的跡象。與他溝通很好。這是非常財富,它擅長運作,職業是仙人的標誌。單身男人有這個夢想,主要生活可以有人幫忙
我不怎麼想?

我不怎麼想?

我做了什麼意味著看到米飯烹飪?我得到了這個夢想,五線的主要土壤,但是Tu Ke水是錢的跡象,職業生涯更加真誠。他真誠地誠實。這是豐富的,這是夏瑞的巨星
夢想你的意思是什麼?

夢想你的意思是什麼?

你是什​​麼意思夢想的夢想?夢想,主要木材的五個要素,水的跡象,主營業務,主營業務,案子應該抓住魅力,不能疏忽,春天夢想的吉利夢想夏天的夢想不幸。詢問學者夢想
拯救夢想

拯救夢想

拯救夢想什麼意思?你夢想著拯救人嗎?拯救人們的夢想有一個現實,也有夢想的主觀想像力,請參閱週宮官方網站拯救人民夢想的詳細解釋。夢想著敵人被拯救出來
2022愛方向和生日是在[質量個性]中

2022愛方向和生日是在[質量個性]中

[救生員]有人說,在出生88天之前,胎兒已經知道哪天的出生,如何有優質的個性,將走在什麼樣的愛情之旅,將與生活生活有什么生活。今天
夢想切割剪裁

夢想切割剪裁

夢想切割剪裁什麼意思?你夢想切你的手是好的嗎?夢想切割手工切割手有一個真正的影響和反應,也有夢想的主觀想像力。請參閱官方網站夢想的細節,以削減手
夢想著親人死了

夢想著親人死了

夢想著親人死了什麼意思?你夢想夢想你的親人死嗎?夢想有一個現實的影響和反應,還有夢想的主觀想像力,請參閱夢想世界夢想死亡的親屬的詳細解釋
夢想搶劫

夢想搶劫

夢想搶劫什麼意思?你夢想搶劫嗎?夢想著搶劫有一個現實的影響和反應,也有夢想的主觀想像力,請參閱週恭吉夢官方網站的詳細解釋。夢想搶劫
夢想缺乏缺乏紊亂

夢想缺乏缺乏紊亂

夢想缺乏缺乏紊亂什麼意思?你夢想缺乏異常藥物嗎?夢想缺乏現實世界的影響和現實,還有夢想的主觀想像,請看官方網站的夢想組織缺乏異常藥物。我覺得有些東西缺失了