Java 輕松理解深拷貝與淺拷貝
點(diǎn)擊上方藍(lán)色字體,選擇“標(biāo)星公眾號”
優(yōu)質(zhì)文章,第一時(shí)間送達(dá)
作者 | aduner
來源 | urlify.cn/yuyYra
前言
本文代碼中有用到一些注解,主要是Lombok與junit用于簡化代碼。
主要是看到一堆代碼會(huì)很亂,這樣理解更清晰。如果沒用過不用太過糾結(jié)。
對象的拷貝(克隆)是一個(gè)非常高頻的操作,主要有以下三種方式:
直接賦值
拷貝:
淺拷貝
深拷貝
因?yàn)镴ava沒有指針的概念,或者說是不需要我們?nèi)ゲ傩模@讓我們省去了很多麻煩,但相應(yīng)的,對于對象的引用、拷貝有時(shí)候就會(huì)有些懵逼,藏下一些很難發(fā)現(xiàn)的bug。
為了避免這些bug,理解這三種操作的作用與區(qū)別就是關(guān)鍵。
直接賦值
用等于號直接賦值是我們平時(shí)最常用的一種方式。
它的特點(diǎn)就是直接引用等號右邊的對象
先來看下面的例子
先創(chuàng)建一個(gè)Person類
@Data
@AllArgsConstructor
@ToString
public class Person{
private String name;
private int age;
private Person friend;
}
測試
@Test
public 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);
}
結(jié)果
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))
分析:
可以看到通過直接賦值進(jìn)行拷貝,其實(shí)就只是單純的對前對象進(jìn)行引用。
如果這些對象都是基礎(chǔ)對象當(dāng)然沒什么問題,但是如果對象進(jìn)行操作,相當(dāng)于兩個(gè)對象同屬一個(gè)實(shí)例。

拷貝
直接賦值雖然方便,但是很多時(shí)候并不是我們想要的結(jié)果,很多時(shí)候我們需要的是兩個(gè)看似一樣但是完全獨(dú)立的兩個(gè)對象。
這種時(shí)候我們就需要用到一個(gè)方法clone()
clone()并不是一個(gè)可以直接使用的方法,需要先實(shí)現(xiàn)Cloneable接口,然后重寫它才能使用。
protected native Object clone() throws CloneNotSupportedException;
clone()方法被native關(guān)鍵字修飾,native關(guān)鍵字說明其修飾的方法是一個(gè)原生態(tài)方法,方法對應(yīng)的實(shí)現(xiàn)不是在當(dāng)前文件,而是系統(tǒng)或者其他語言來實(shí)現(xiàn)。
淺拷貝
淺拷貝可以實(shí)現(xiàn)對象克隆,但是存在一些缺陷。
定義:
如果原型對象的成員變量是值類型,將復(fù)制一份給克隆對象,也就是在堆中擁有獨(dú)立的空間;
如果原型對象的成員變量是引用類型,則將引用對象的地址復(fù)制一份給克隆對象,指向相同的內(nèi)存地址。
舉例
光看定義不太好一下子理解,上代碼看例子。
我們先來修改一下Person類,實(shí)現(xiàn)Cloneable接口,重寫clone()方法,其實(shí)很簡單,只需要用super調(diào)用一下即可
@Data
@AllArgsConstructor
@ToString
public 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
@AllArgsConstructor
public class Friend {
private String Name;
}
測試
@Test
public 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);
}
結(jié)果
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淺拷貝的原理其實(shí)是把原對象的各個(gè)屬性的地址拷貝給新對象。
注意我說的是各個(gè)屬性,就算是基礎(chǔ)對象屬性其實(shí)也是拷貝的地址。
你可能有點(diǎn)暈了,都是拷貝了地址,為什么修改了 person1 對象的 name age 屬性值,person2 對象的 name age 屬性值沒有改變呢?
我們一步步來,拿name屬性來說明:
String、Integer 等包裝類都是不可變的對象
當(dāng)需要修改不可變對象的值時(shí),需要在內(nèi)存中生成一個(gè)新的對象來存放新的值
然后將原來的引用指向新的地址
我們修改了
person1對象的name屬性值,person1對象的name字段指向了內(nèi)存中新的String對象我們并沒有改變
person2對象的 name 字段的指向,所以person2對象的name還是指向內(nèi)存中原來的String地址
看圖

這個(gè)圖已經(jīng)很清晰的展示了其中的過程,因?yàn)?/span>person1 對象改變friend時(shí)是改變的引用對象的屬性,并不是新建立了一個(gè)對象進(jìn)行替換,原本老王的消失了,變成了小王。所以person2也跟著改變了。
深拷貝
深拷貝就是我們拷貝的初衷了,無論是值類型還是引用類型都會(huì)完完全全的拷貝一份,在內(nèi)存中生成一個(gè)新的對象。
拷貝對象和被拷貝對象沒有任何關(guān)系,互不影響。
深拷貝相比于淺拷貝速度較慢并且花銷較大。
簡而言之,深拷貝把要復(fù)制的對象所引用的對象都復(fù)制了一遍。

因?yàn)镴ava本身的特性,對于不可變的基本值類型,無論如何在內(nèi)存中都是只有一份的。
所以對于不可變的基本值類型,深拷貝跟淺拷貝一樣,不過并不影響什么。
實(shí)現(xiàn):
想要實(shí)現(xiàn)深拷貝并不難,只需要在淺拷貝的基礎(chǔ)上進(jìn)行一點(diǎn)修改即可。
給friend添加一個(gè)
clone()方法。在
Person類的clone()方法調(diào)用friend的clone()方法,將friend也復(fù)制一份即可。
@Data
@ToString
public 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
@AllArgsConstructor
public class Friend implements Cloneable{
private String Name;
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
測試
@Test
public 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);
}
結(jié)果
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=老王))
分析:
可以看到這次是真正的完全獨(dú)立了起來。
需要注意的是,如果Friend類本身也存在引用類型,則需要在Friend類中的clone(),也去調(diào)用其引用類型的clone()方法,就如是Person類中那樣,對!就是套娃!
所以對于存在多層依賴關(guān)系的對象,實(shí)現(xiàn)Cloneable接口重寫clone()方法就顯得有些笨拙了。
這里我們在介紹一種方法:利用序列化實(shí)現(xiàn)深拷貝
Serializable 實(shí)現(xiàn)深拷貝
修改Person和Friend,實(shí)現(xiàn)Serializable接口
@Data
@ToString
public 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
@AllArgsConstructor
public class Friend implements Serializable {
private String Name;
}
測試
@Test
public 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);
}
結(jié)果
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=老王))
只要將會(huì)被復(fù)制到的引用對象標(biāo)記Serializable接口,通過序列化到方式即可實(shí)現(xiàn)深拷貝。
原理:
對象被序列化成流后,因?yàn)閷懺诹骼锏氖?span style="margin-right: 3px;margin-left: 3px;">對象的一個(gè)拷貝,而原對象仍然存在于虛擬機(jī)里面。
通過反序列化就可以獲得一個(gè)完全相同的拷貝。
利用這個(gè)特性就實(shí)現(xiàn)了對象的深拷貝。
總結(jié)
直接賦值是將新的對象指向原對象所指向的實(shí)例,所以一旦有所修改,兩個(gè)對象會(huì)一起變。
淺拷貝是把原對象屬性的地址傳給新對象,對于不可變的基礎(chǔ)類型,實(shí)現(xiàn)了二者的分離,但對于引用對象,二者還是會(huì)一起改變。
深拷貝是真正的完全拷貝,二者沒有關(guān)系。實(shí)現(xiàn)深拷貝時(shí)如果存在多層依賴關(guān)系,可以采用序列化的方式來進(jìn)行實(shí)現(xiàn)。
對于
Serializable接口、Cloneable接口,其實(shí)都是相當(dāng)于一個(gè)標(biāo)記,點(diǎn)進(jìn)去看源碼,其實(shí)他們是一個(gè)空接口。
鋒哥最新SpringCloud分布式電商秒殺課程發(fā)布
??????
??長按上方微信二維碼 2 秒
感謝點(diǎn)贊支持下哈 
