Java 物件拷貝是為物件賦值的一種方式,簡單來說就是創建一個和原物件相同的物件,新創建的物件是原物件的一個副本,面試官賊拉喜歡在面試的時候問一問你淺拷貝和深拷貝的原理,因為它涉及到物件的參考關系,涉及到 Java 是傳值還是傳遞參考關系,這通常是面試的重點,所以在聊深拷貝和淺拷貝之前,我們先來聊一聊參考關系,
關于參考
在 Java 中,除了基本資料型別(四類八種資料型別)之外,還存在參考資料型別,一般使用 = 號做賦值操作的時候,對于基本資料型別,實際上是拷貝的它的值,但是對于物件而言,其實賦值的只是這個物件的參考,也就是將原物件的參考傳遞過去,但是他們實際上還是指向的同一個物件,如下代碼所示
public class Food{
String name;
int num;
String taste;
constructor()
get and set()
toString()
}
測驗類:
public static void main(String[] args) {
int i1 = 10;
int i2 = i1; // 基本資料型別的拷貝,拷貝值
System.out.println("i2 = " + i2);
Food milk = new Food("milk",1,"fragrance");
Food food = milk;
System.out.printf("food = " + food);
System.out.println("milk = " + milk); // milk 和 food 都指向同一個堆記憶體物件
}
如果用圖表示的話,應該是下面這樣的:

不用糾結 Java 中到底是值傳遞還是參考傳遞這種無意義的爭論中,你只要記得對于基本資料型別,傳遞的是資料型別的值,而對于參考型別來說,傳遞的是物件的參考,也就是物件的地址就可以了,
關于淺拷貝和深拷貝
淺拷貝和深拷貝其實就是在參考的這個基礎上來做區分的,如果在拷貝的時候,只對基本資料型別進行拷貝,對參考資料型別只是進行了參考的傳遞,沒有真正的創建一個新的物件,這種拷貝方式就認為是淺拷貝,反之,在對參考資料型別進行拷貝的時候,創建了一個新的物件,并且復制其內的成員變數,這種拷貝方式就被認為是深拷貝,
淺拷貝
那么如何實作淺拷貝(Shallow copy)呢?很簡單,就是在需要拷貝的類上實作 Cloneable 介面并重寫其 clone() 方法就可以了,
下面我們對 Food 類進行修改,我們讓他實作 Cloneable 介面,并重寫 clone() 方法,
public class Food implements Cloneable{
...
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
...
}
然后在測驗類中的代碼如下
Food milk = new Food("milk",1,"fragrance");
Food food = (Food)milk.clone();
System.out.println("milk = " + milk);
System.out.println("food = " + food);
可以看到,現在的 food 物件是由 milk 物件拷貝出來的,那么此時的 food 物件和 milk 物件是同一個物件嗎?我們通過列印,可以看到這兩個物件的原生 hashcode,
milk = com.cxuan.objectclone.Food@3cd1a2f1
food = com.cxuan.objectclone.Food@4d7e1886
可以發現,food 和 milk 并不是同一個物件,那 milk 中還有三個屬性值,這三個屬性值在 food 中是不是也一樣呢?為了驗證這個猜想,我們重寫了 toString 方法,
@Override
public String toString() {
return "Food{" +
"name='" + name + '\'' +
", num=" + num +
", taste='" + taste + '\'' +
'}';
}
然后再次列印 food 和 milk ,可以觀察到如下結果
milk = Food{name='milk', num=1, taste='fragrance'}
food = Food{name='milk', num=1, taste='fragrance'}
嗯哼,雖然看起來"cxuan 哥"和"cuan 哥"是兩種完全不同的稱呼!但是他們卻有一種共同的能力:寫作!
我們還是通過圖示來說明一下:

這幅圖看出門道了么?在堆區分別出現了兩個 Food 物件,這同時表明 clone 方法會重新創建一個物件并為其分配一塊記憶體區域;雖然出現了兩個物件,但是兩個物件中的屬性值是一樣的,這也是換湯不換藥,雖然湯和藥是不同的東西(物件),但是他們都溶于水(屬性值),
深拷貝
雖然淺拷貝是一種換湯不換藥的說法,但是在 Java 世界中還是有一種說法是,,,,,,是啥來著?
詞窮了,,,,,,

哦對,還有一種改頭換面的形式,它就是我們所熟悉的深拷貝(Deep copy),先來拋出一下深拷貝的定義:在進行物件拷貝的基礎上,對物件的成員變數也依次拷貝的方式被稱為深拷貝,
哈哈哈哈,這故作高深的深拷貝原來就是在淺拷貝的基礎上再復制一下它的屬性值啊,我還以為是啥高深的東西呢!上代碼!
我們先增加一個飲品類 Drink ,
public class Drink implements Cloneable {
String name;
get and set()
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
toString()
}
然后更改一下 Food 類,因為 Drink 也算是 Food ,所以我們在 Food 類中增加對 Drink 的參考,然后再修改 get set 、toString 、clone 、構造方法,修改后的 Food 類代碼如下
public class Food implements Cloneable{
String name;
int num;
String taste;
Drink drink;
public Food(String name, int num, String taste,Drink drink) {
this.name = name;
this.num = num;
this.taste = taste;
this.drink = drink;
}
get and set...
@Override
protected Object clone() throws CloneNotSupportedException {
Food food = (Food)super.clone();
food.drink = (Drink) drink.clone();
return super.clone();
}
@Override
public String toString() {
return "Food{" +
"name='" + name + '\'' +
", num=" + num +
", taste='" + taste + '\'' +
", drink=" + drink +
'}';
}
}
可以看到最大的改變是 clone 方法,我們在 clone 方法中,實作了對 Food 物件的拷貝,同時也實作了對 Drink 物件的拷貝,這就是我們上面所說的復制物件并復制物件的成員變數,
然后我們進行一下 Deep Copy的測驗:
public static void main(String[] args) throws CloneNotSupportedException {
Drink drink = new Drink("milk");
Food food = new Food("humberge",1,"fragrance",drink);
Food foodClone = (Food)food.clone();
Drink tea = new Drink("tea");
food.setDrink(tea);
System.out.println("food = " + food);
System.out.println("foodClone = " + foodClone.getDrink());
}
運行完成后的輸出結果如下:
food = Food{name='humberge', num=1, taste='fragrance', drink=Drink{name='tea'}}
foodClone = Drink{name='milk'}
可以看到,我們把 foodClone 拷貝出來之后,修改 food 中的 drink 變數,卻不會對 foodClone 造成改變,這就說明 foodClone 已經成功實作了深拷貝,
用圖示表示的話,應該是下面這樣的:

這是深拷貝之后的記憶體分配圖,現在可以看到,food 和 foodClone 完全是兩個不同的物件,它們之間不存在紐帶關系,
我們上面主要探討實作物件拷貝的方式是物件實作 Cloneable 介面,并且呼叫重寫之后的 clone 方法,在 Java 中,還有一種實作物件拷貝的方式是使用 序列化,
序列化
使用序列化的方式主要是使用 Serializable 介面,這種方式還以解決多層拷貝的問題,多層拷貝就是參考型別里面又有參考型別,層層嵌套下去,使用 Serializable 的關鍵代碼如下
public Person clone() {
Person person = null;
try {
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;
}
使用序列化可以實作深拷貝,它的原理是將二進制位元組流內容寫到一個文本或位元組陣列,然后是從這個文本或者位元組陣列中讀取資料,原物件寫入這個文本或者位元組陣列后再拷貝給 clone 物件,原物件的修改不會影響 clone 物件,因為 clone 物件是從文本或者位元組陣列中讀取的,
如何選擇拷貝方式
到現在我們已經把淺拷貝和深拷貝都介紹完了,那么如何選擇淺拷貝和深拷貝呢?下面是幾點注意事項??
-
如果物件的屬性都是基本資料型別,那么可以使用淺拷貝,
-
如果物件有參考型別,那就要基于具體的需求來選擇淺拷貝還是深拷貝,
-
如果物件嵌套層數比較多,推薦使用 Serializable 介面實作深拷貝,
-
如果物件參考任何時候都不會被改變,那么沒必要使用深拷貝,只需要使用淺拷貝就行了,如果物件參考經常改變,那么就要使用深拷貝,沒有一成不變的規則,一切都取決于具體需求,
其他拷貝方式
除了物件的拷貝,Java 中還提供了其他的拷貝方式
比如陣列的拷貝,你可以使用 Arrays.copyof 實作陣列拷貝,還可以使用默認的 clone 進行拷貝,不過這兩者都是淺拷貝,
public void test() {
int[] lNumbers1 = new int[5];
int[] rNumbers1 = Arrays.copyOf(lNumbers1, lNumbers1.length);
int[] lNumbers2 = new int[5];
int[] rNumbers2 = lNumbers2.clone();
}
除了基本陣列資料型別之外的拷貝,還有物件的拷貝,不過用法基本是一樣的,
集合也可以實作拷貝,因為集合的底層就使用的是陣列,所以用法也是一樣的,
一些說明
針對 Cloneable 介面,有下面三點使用說明
-
如果類實作了 Cloneable 介面,再呼叫 Object 的 clone() 方法可以合法地對該類實體進行按欄位復制,
-
如果在沒有實作 Cloneable 介面的實體上呼叫 Object 的 clone() 方法,則會導致拋出
CloneNotSupporteddException, -
實作此介面的類應該使用公共方法重寫 Object 的clone() 方法,因為 Object 的 clone() 方法是一個受保護的方法,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/294926.html
標籤:java
下一篇:Java - 類和物件
