前言
本文程式碼中有用到一些註解,主要是Lombok與junit用於簡化程式碼。
主要是看到一堆程式碼會很亂,這樣理解更清晰。如果沒用過不用太過糾結。
物件的拷貝(克隆)是一個非常高頻的操作,主要有以下三種方式:
- 直接賦值
- 拷貝:
- 淺拷貝
- 深拷貝
因為Java沒有指標的概念,或者說是不需要我們去操心,這讓我們省去了很多麻煩,但相應的,對於物件的引用、拷貝有時候就會有些懵逼,藏下一些很難發現的bug。
為了避免這些bug,理解這三種操作的作用與區別就是關鍵。
直接賦值
用等於號直接賦值是我們平時最常用的一種方式。
它的特點就是直接引用等號右邊的物件
先來看下面的例子
先建立一個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);
}
結果
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
@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);
}
結果
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
@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);
}
結果
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
@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);
}
結果
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
介面,其實都是相當於一個標記,點進去看原始碼,其實他們是一個空介面。