一、基本資料型別 & 引用型別
1.1 基本概念
在討論 淺拷貝 & 深拷貝 這個問題之前,我們需要先了解 基本資料型別 & 引用型別 這兩者之間的區別,否則後面會很疑惑。在Java
當中,這兩類的代表分別為:
- 八種 基本資料型別:
byte
、short
、int
、long
、float
、double
、char
、boolean
。 - 引用型別:除去基本資料型別的其它型別都是引用資料型別,例如類、介面、陣列。
在 (1) JAVA 基本資料型別與引用資料型別 一文中總結了這兩者的區別:
基本資料型別 | 引用資料型別 |
---|---|
變數名指向具體的數值 | 變數名指向存資料物件的記憶體地址,即變數名指向hash 值 |
變數在宣告之後就會立刻分配給他記憶體空間 | 它以特殊的方式指向物件實體,這類變數宣告時不會分配記憶體,只是儲存了一個記憶體地址 |
基本型別之間的賦值是建立新的拷貝 | 物件之間的賦值只是傳遞引用 |
“==”和“!=”是在比較值 | “==” 和“!=” 是在比較兩個引用是否相同 |
使用時需要賦具體值,判斷時使用== 號 |
使用時可以賦值null ,判斷時使用equals 方法 |
1.2 基本資料型別 和 引用資料型別 傳遞區別
程式設計語言中有關引數傳遞給方法的兩個專業術語是:
- 按值呼叫:表示方法接收的是呼叫者提供的值。
- 按引用呼叫:表示方法接收的是呼叫者提供的變數的地址。
在Java
中 不存在按引用呼叫,也就是說,假如方法傳遞的是一個引用資料型別,那麼可以修改 引用所指向的物件的屬性,但不能 讓引用指向其它的物件。
1.2.1 傳遞基本資料型別
public static void methodBasic() {
int lNum = 3;
methodRunInner(lNum);
Log.d("CopySample", "After methodRunInner, lNum=" + lNum);
}
private static void methodRunInner(int lNum) {
lNum++;
Log.d("CopySample", "methodRunInner, lNum=" + lNum);
}
複製程式碼
![傳遞基本資料型別](https://i.iter01.com/images/8ac1136790011645fa3194695f20d0c20e8c4beda45bccdc069501d8f12c8d02.png)
1.2.2 傳遞引用資料型別
當傳遞的是引用資料型別,可以在函式中修改該引用所指向的物件的成員變數的值,如下所示:
public static void methodRef() {
NumHolder holder = new NumHolder();
holder.num = 3;
methodRunInner(holder);
Log.d("CopySample", "After methodRunInner, holder.num=" + holder.num);
}
private static void methodRunInner(NumHolder holder) {
holder.num++;
Log.d("CopySample", "methodRunInner, holder.num=" + holder.num);
}
private static class NumHolder {
int num;
}
複製程式碼
![傳遞引用資料型別](https://i.iter01.com/images/de290d68d5cf16ddd4b30e265aece7d34c6fac53038411e0eec15a593179fb2c.png)
public static void methodSwapRef() {
NumHolder lHolder = new NumHolder();
NumHolder rHolder = new NumHolder();
lHolder.num = 3;
rHolder.num = 4;
methodRunInner(lHolder, rHolder);
Log.d("CopySample", "methodSwapRef, lHolder.num=" + lHolder.num + ", rHolder.num=" + rHolder.num);
}
private static void methodRunInner(NumHolder lHolder, NumHolder rHolder) {
NumHolder temp = lHolder;
lHolder = rHolder;
rHolder = temp;
Log.d("CopySample", "methodRunInner, lHolder.num=" + lHolder.num + ", rHolder.num=" + rHolder.num);
}
private static class NumHolder {
int num;
}
複製程式碼
![在方法中交換引用](https://i.iter01.com/images/7bf7fe043acdb3cffd5cd587f971d98db88f82b85c0fbcf81ce544adb0fd5aa3.png)
二、淺拷貝 Vs 深拷貝
在對基本資料型別和引用資料型別瞭解之後,我們就可以開始分析淺拷貝 & 深拷貝了。
2.1 定義
首先讓我們來看一下它們倆的定義:
- 淺拷貝:使用一個已知例項對新建立例項的成員變數逐個 賦值,這個方式被稱為淺拷貝。
- 深拷貝:當一個類的拷貝構造方法,不僅要複製物件的所有非引用成員變數值,還要為引用型別的成員變數建立新的例項,並且初始化為形式引數例項值。
2.2 賦值操作符 =
上面我們講到了一個關鍵詞 - 賦值,那麼讓我們來先看一下賦值操作符=
在基本資料型別和引用資料型別上會發生什麼。
2.2.1 基本資料型別
public static void startRun1() {
int lNumber = 2;
int rNumber = lNumber;
rNumber = 3;
Log.d("CopySample", "lNumber=" + lNumber + ",rNumber=" + rNumber);
}
複製程式碼
執行結果為:
![基本資料型別 - 賦值結果](https://i.iter01.com/images/f3e277106b7e085d1ea9de25b85b6c0f7c62e6d4b6c10d61b2274c3eaec46130.png)
rNumber
對lNumber
進行賦值的時候,實際上是開闢了一塊新的記憶體空間,因此對於rNumber
的改變,並不會影響lNumber
。
2.2.2 引用資料型別
public static void startRun2() {
People lPeople = new People();
lPeople.age = 10;
People rPeople = lPeople;
rPeople.age = 20;
Log.d("CopySample", "lPeople=" + lPeople + ",lPeople.age=" + lPeople.age + ",\n" +
"rPeople=" + rPeople + ",rPeople.age=" + rPeople.age);
}
public static class People {
int age;
}
複製程式碼
![引用資料型別 - 賦值結果](https://i.iter01.com/images/e1d56263008d836a3e6e07981157065d88e42ccd3992eb56d3286dd070689034.png)
=
後,lPeople
和rPeople
指向同一塊記憶體地址(即上圖中的@f7a53fd
),通過該地址我們可以訪問到它儲存的物件。
所以,當我們通過rPeople
來改變成員變數age
的值之後,通過lPeople
訪問該成員變數可以看到更新後的值,這就是 基本資料型別和引用資料型別賦值的區別。
2.3 淺拷貝
淺拷貝的前提是該類了實現Cloneable
介面,並重寫clone
方法。在拷貝某個物件時,呼叫該物件的clone
方法返回一個新的物件,該物件就是淺拷貝的結果。
public static void startRun3() {
People lPeople = new People();
lPeople.age = 10;
lPeople.holder = new People.Holder();
lPeople.holder.holderValue = 10;
People rPeople = (People) lPeople.clone();
rPeople.age = 20;
rPeople.holder.holderValue = 20;
Log.d("CopySample", "lPeople=" + lPeople + ",lPeople.age=" + lPeople.age + ",lPeople.holder=" + lPeople.holder + ",\n" +
"rPeople=" + rPeople + ",rPeople.age=" + rPeople.age + ",rPeople.holder=" + rPeople.holder);
Log.d("CopySample", "lHolderValue=" + lPeople.holder.holderValue + ",rHolderValue=" + rPeople.holder.holderValue);
}
public static class People implements Cloneable {
int age;
Holder holder;
@Override
protected Object clone() {
try {
return super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return null;
}
public static class Holder {
int holderValue;
}
}
複製程式碼
![淺拷貝](https://i.iter01.com/images/99caffee44ab7ceb57571c3c70f86275949a84bf7a1be8e8da62891ecb752bfb.png)
@f7a53fd
,拷貝的物件在@3a8bb43
),但是這並不意味著這兩者之間一定是完全獨立的,因為clone
方法的預設實現,對類中不同型別的成員變數會有不同的表現。
- 基本資料型別:對該成員變數進行復制。
- 引用資料型別:複製引用,但不會開闢新的記憶體空間,因此被拷貝物件的該成員變數與拷貝物件對應的成員變數 指向同一塊記憶體地址(上圖中的
@f5b08f2
),就和我們在2.2.2
中看到的一樣。
正是由於這一區別,當我們通過rPeople
對其成員變數進行修改時,lPeople
的age
屬性(基本資料型別)不受影響,而holder.holderValue
(引用資料型別)卻會跟著改變。
2.4 深拷貝
下面,我們先來演示如何對2.2
中的例子進行改進,實現深拷貝。我們讓Holder
類也實現了Clonable
介面,因此呼叫它的clone
方法後會返回一個新物件的引用;之後,還要重寫People
的clone
方法,對clone
之後的物件中的成員變數holder
採用clone
方法進行拷貝。
public static void startRun3() {
People lPeople = new People();
lPeople.age = 10;
lPeople.holder = new People.Holder();
lPeople.holder.holderValue = 10;
People rPeople = (People) lPeople.clone();
rPeople.age = 20;
rPeople.holder.holderValue = 20;
Log.d("CopySample", "lPeople=" + lPeople + ",lPeople.age=" + lPeople.age + ",lPeople.holder=" + lPeople.holder + ",\n" +
"rPeople=" + rPeople + ",rPeople.age=" + rPeople.age + ",rPeople.holder=" + rPeople.holder);
Log.d("CopySample", "lHolderValue=" + lPeople.holder.holderValue + ",rHolderValue=" + rPeople.holder.holderValue);
}
public static class People implements Cloneable {
int age;
Holder holder;
@Override
protected Object clone() {
try {
People people = (People) super.clone();
people.holder = (People.Holder) this.holder.clone();
return people;
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return null;
}
public static class Holder implements Cloneable {
int holderValue;
@Override
protected Object clone() {
try {
return super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return null;
}
}
}
複製程式碼
![深拷貝](https://i.iter01.com/images/2b7eef45f2ed9affe8d7c916482a5b6af408b137132756efa93d4a98a54ba2ac.png)
rPeople
的holder
引用指向了一塊新的記憶體地址@b855c0
,因此對它的holderValue
進行改變並不會影響到lPeople
的holder
變數中的holderValue
。
三、陣列 & 集合的拷貝
3.1 陣列的拷貝
陣列除了預設實現了clone()
方法之外,還提供了Arrays.copyOf
方法用於拷貝,這兩者都是淺拷貝。
3.1.1 基本資料型別陣列
public static void startRun4() {
int[] lNumbers1 = new int[5];
int[] rNumbers1 = lNumbers1;
rNumbers1[0] = 1;
Log.d("CopySample", "lNumbers1[0]=" + lNumbers1[0] + ",rNumbers1[0]=" + rNumbers1[0]);
int[] lNumbers2 = new int[5];
int[] rNumbers2 = Arrays.copyOf(lNumbers2, lNumbers2.length);
rNumbers2[0] = 1;
Log.d("CopySample", "lNumbers2[0]=" + lNumbers2[0] + ",rNumbers2[0]=" + rNumbers2[0]);
int[] lNumbers3 = new int[5];
int[] rNumbers3 = lNumbers3.clone();
rNumbers3[0] = 1;
Log.d("CopySample", "lNumbers3[0]=" + lNumbers3[0] + ",rNumbers3[0]=" + rNumbers3[0]);
}
複製程式碼
執行結果:
![基本資料型別陣列](https://i.iter01.com/images/7639d7a3c9e59026a407540cfc7a140fb5f0f243b86ddd63956a6376c63dc6ce.png)
3.1.2 引用資料型別陣列
public static void startRun5() {
People[] lNumbers1 = new People[5];
lNumbers1[0] = new People();
People[] rNumbers1 = lNumbers1;
Log.d("CopySample", "lNumbers1[0]=" + lNumbers1[0] + ",rNumbers1[0]=" + rNumbers1[0]);
People[] lNumbers2 = new People[5];
lNumbers2[0] = new People();
People[] rNumbers2 = Arrays.copyOf(lNumbers2, lNumbers2.length);
Log.d("CopySample", "lNumbers2[0]=" + lNumbers2[0] + ",rNumbers2[0]=" + rNumbers2[0]);
People[] lNumbers3 = new People[5];
lNumbers3[0] = new People();
People[] rNumbers3 = lNumbers3.clone();
Log.d("CopySample", "lNumbers3[0]=" + lNumbers3[0] + ",rNumbers3[0]=" + rNumbers3[0]);
}
public static class People implements Cloneable {
int age;
Holder holder;
@Override
protected Object clone() {
try {
return super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return null;
}
public static class Holder {
int holderValue;
}
}
複製程式碼
![引用資料型別陣列](https://i.iter01.com/images/c0ff856375055b4a3f1a559f92617b1f3554ff43d6c2c5a2b3375deca8e6225a.png)
3.2 集合的拷貝
集合的拷貝也是我們平時經常會遇到的,一般情況下,我們都是用淺拷貝來實現,即通過建構函式或者clone
方法。
3.2.1 建構函式和 clone() 預設都是淺拷貝
public static void listShallowRun() {
ArrayList<People> lPeoples = new ArrayList<>();
People people1 = new People();
lPeoples.add(people1);
Log.d("CopySample", "listShallowRun, lPeoples[0]=" + lPeoples.get(0));
ArrayList<People> rPeoples = (ArrayList<People>) lPeoples.clone();
Log.d("CopySample", "listShallowRun, rPeoples[0]=" + rPeoples.get(0));
}
public static class People implements Cloneable {
int age;
Holder holder;
@Override
protected Object clone() {
try {
People people = (People) super.clone();
people.holder = (People.Holder) this.holder.clone();
return people;
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return null;
}
public static class Holder implements Cloneable {
int holderValue;
@Override
protected Object clone() {
try {
return super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return null;
}
}
}
複製程式碼
![集合的淺拷貝](https://i.iter01.com/images/cc4fd62a02eeaeef1d26e315325f55f03f304400825b7799f4253e797186680f.png)
3.2.2 實現集合的深拷貝
在某些特殊情況下,如果需要實現集合的深拷貝,那就要建立一個新的集合,然後通過深拷貝原先集合中的每個元素,將這些元素加入到新的集合當中。
public static void listDeepRun() {
ArrayList<People> lPeoples = new ArrayList<>();
People people1 = new People();
people1.holder = new People.Holder();
lPeoples.add(people1);
Log.d("CopySample", "listShallowRun, lPeoples[0]=" + lPeoples.get(0));
ArrayList<People> rPeoples = new ArrayList<>();
for (People people : lPeoples) {
rPeoples.add((People) people.clone());
}
Log.d("CopySample", "listShallowRun, rPeoples[0]=" + rPeoples.get(0));
}
public static class People implements Cloneable {
int age;
Holder holder;
@Override
protected Object clone() {
try {
People people = (People) super.clone();
people.holder = (People.Holder) this.holder.clone();
return people;
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return null;
}
public static class Holder implements Cloneable {
int holderValue;
@Override
protected Object clone() {
try {
return super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return null;
}
}
}
複製程式碼
![採用 clone 實現集合的深拷貝](https://i.iter01.com/images/5dc325c7d4ee085fe68b5a915d8607225258ee2faa9de7d8d3b3bf3b3a58dcd6.png)
四、參考文獻
(1) Java 基本資料型別與引用資料型別
(2) Java 基本資料型別傳遞與引用傳遞區別詳解
(3) 詳解 Java 中的 clone 方法 -- 原型模式
(4) Java List 複製:淺拷貝與深拷貝
(5) 漸析 Java 的淺拷貝和深拷貝