天天看點

Java 淺拷貝、深拷貝,你知多少?

        這是今天我們在技術群裡面讨論的一個 Java 知識點,讨論的相當激烈,由于對這一塊使用的比較少,是以對這一塊多少有些盲區。這篇文章總結了所讨論的内容,希望這篇文章對你有所幫助。    在 Java 開發中,對象拷貝或者說對象克隆是常有的事,對象克隆最終都離不開直接指派、淺拷貝、深拷貝 這三種方式,其中直接指派應該是我們最常用的一種方式吧,對于淺拷貝和深拷貝可能用的少,是以或多或少存在一些誤區,這篇文章會詳細的介紹這三種對象克隆方式。

值類型:Java 的基本資料類型,例如 int、float

引用類型:自定義類和 Java 包裝類(string、integer)  直接指派    直接指派是我們最常用的方式,在我們代碼中的展現是<code>Persona = new Person();Person b = a</code>,是一種簡單明了的方式,但是它隻是拷貝了對象引用位址而已,并沒有在記憶體中生成新的對象,我們可以通過下面這個例子來證明這一點// person 對象

public class Person {

// 姓名

private String name;

// 年齡

private int age;

// 郵件

private String email;

// 描述

private String desc;

...省略get/set...

}

// main 方法

public class PersonApp {

public static void main(String[] args) {

// 初始化一個對象

Person person = new Person("張三",20,"[email protected]","我是張三");

// 複制對象

Person person1 = person;

// 改變 person1 的屬性值

person1.setName("我不是張三了");

System.out.println("person對象:"+person);

System.out.println("person1對象:"+person1);

}運作上面代碼,你會得到如下結果:person對象:Person{name='我不是張三了', age=20, email='[email protected]', desc='我是張三'}

person1對象:Person{name='我不是張三了', age=20, email='[email protected]', desc='我是張三'}        我們将 person 對象複制給了 person1 對象,我們對 person1 對象的 name 屬性進行了修改,并未修改 person 對象的name 屬性值,但是我們最後發現 person 對象的 name 屬性也發生了變化,其實不止這一個值,對于其他值也是一樣的,是以這結果證明了我們上面的結論:直接指派的方式沒有生産新的對象,隻是生新增了一個對象引用,直接指派在 Java 記憶體中的模型大概是這樣的

Java 淺拷貝、深拷貝,你知多少?

淺拷貝        淺拷貝也可以實作對象克隆,從這名字你或許可以知道,這種拷貝一定存在某種缺陷,是的,它就是存在一定的缺陷,先來看看淺拷貝的定義:如果原型對象的成員變量是值類型,将複制一份給克隆對象,也就是說在堆中擁有獨立的空間;如果原型對象的成員變量是引用類型,則将引用對象的位址複制一份給克隆對象,也就是說原型對象和克隆對象的成員變量指向相同的記憶體位址。換句話說,在淺克隆中,當對象被複制時隻複制它本身和其中包含的值類型的成員變量,而引用類型的成員對象并沒有複制。可能你沒太了解這段話,那麼我們在來看看淺拷貝的通用模型:

Java 淺拷貝、深拷貝,你知多少?

淺拷貝通用模型        要實作對象淺拷貝還是比較簡單的,隻需要被複制類需要實作 Cloneable 接口,重寫 clone 方法即可,對 person 類進行改造,使其可以支援淺拷貝。public class Person implements Cloneable {

/*

* 重寫 clone 方法,需要将權限改成 public ,直接調用父類的 clone 方法就好了

*/

@Override

public Object clone() throws CloneNotSupportedException {

return super.clone();

...省略...

}        改造很簡單隻需要讓 person 繼承 Cloneable 接口,并且重寫 clone 方法即可,clone 也非常簡單隻需要調用 object 的 clone 方法就好,唯一需要注意的地方就是 clone 方法需要用 public 來修飾,在簡單的修改 main 方法public class PersonApp {

public static void main(String[] args) throws Exception {

Person person1 = (Person) person.clone();

person1.setName("我是張三的克隆對象");

// 修改 person age 的值

person1.setAge(22);

System.out.println();

}重新運作 main 方法,結果如下:person對象:Person{name='張三', age=20, email='[email protected]', desc='我是張三'}

person1對象:Person{name='我是張三的克隆對象', age=22, email='[email protected]', desc='我是張三'}        看到這個結果,你是否有所質疑呢?說好的引用對象隻是拷貝了位址,為啥修改了 person1 對象的 name 屬性值,person 對象沒有改變?這裡就是一個非常重要的知識點了,,原因在于:String、Integer 等包裝類都是不可變的對象,當需要修改不可變對象的值時,需要在記憶體中生成一個新的對象來存放新的值,然後将原來的引用指向新的位址,是以在這裡我們修改了 person1 對象的 name 屬性值,person1 對象的 name 字段指向了記憶體中新的 name 對象,但是我們并沒有改變 person 對象的 name 字段的指向,是以 person 對象的 name 還是指向記憶體中原來的 name 位址,也就沒有變化這種引用是一種特列,因為這些引用具有不可變性,并不具備通用性,是以我們就自定義一個類,來示範淺拷貝,我們定義一個 PersonDesc 類用來存放person 對象中的 desc 字段,,然後在 person 對象中引用 PersonDesc 類,具體代碼如下:// 新增 PersonDesc

public class PersonDesc {

public class Person implements Cloneable {

// 将原來的 string desc 變成了 PersonDesc 對象,這樣 personDesc 就是引用類型

private PersonDesc personDesc;

public void setDesc(String desc) {

this.personDesc.setDesc(desc);

public Person(String name, int age, String email, String desc) {

this.name = name;

this.age = age;

this.email = email;

this.personDesc = new PersonDesc();

}修改 main 方法public class PersonApp {

Person person = new Person("平頭哥",20,"[email protected]","我的公衆号是:平頭哥的技術博文");

person1.setName("我是平頭哥的克隆對象");

person1.setDesc("我已經關注了平頭哥的技術博文公衆号");

}運作 main 方法,得到如下結果:person對象:Person{name='平頭哥', age=20, email='[email protected]', desc='我已經關注了平頭哥的技術博文公衆号'}

person1對象:Person{name='我是平頭哥的克隆對象', age=22, email='[email protected]', desc='我已經關注了平頭哥的技術博文公衆号'}    我們修改 person1 的 desc 字段之後,person 的 desc 也發生了改變,這說明 person 對象和 person1 對象指向是同一個 PersonDesc 對象位址,這也符合淺拷貝引用對象隻拷貝引用位址并未建立新對象的定義,到這你應該知道淺拷貝了吧。深拷貝

    深拷貝也是對象克隆的一種方式,相對于淺拷貝,深拷貝是一種完全拷貝,無論是值類型還是引用類型都會完完全全的拷貝一份,在記憶體中生成一個新的對象,簡單點說就是拷貝對象和被拷貝對象沒有任何關系,互不影響。深拷貝的通用模型如下:

Java 淺拷貝、深拷貝,你知多少?

深拷貝通用模型    深拷貝有兩種方式,一種是跟淺拷貝一樣實作 Cloneable 接口,另一種是實作 Serializable 接口,用序列化的方式來實作深拷貝,我們分别用這兩種方式來實作深拷貝實作 Cloneable 接口方式實作  Cloneable 接口的方式跟淺拷貝相差不大,我們需要引用對象也實作 Cloneable 接口,具體代碼改造如下:public class PersonDesc implements Cloneable{

/**

* clone 方法不是簡單的調用super的clone 就好,

Person person = (Person)super.clone();

// 需要将引用對象也克隆一次

person.personDesc = (PersonDesc) personDesc.clone();

return person;

}main 方法不需要任何改動,我們再次運作 main 方法,得到如下結果:person對象:Person{name='平頭哥', age=20, email='[email protected]', desc='我的公衆号是:平頭哥的技術博文'}

person1對象:Person{name='我是平頭哥的克隆對象', age=22, email='[email protected]', desc='我已經關注了平頭哥的技術博文公衆号'}    可以看出,修改 person1 的 desc 時對 person 的 desc 已經沒有影響了,說明進行了深拷貝,在記憶體中重新生成了一個新的對象。    實作 Serializable 接口方式        實作 Serializable 接口方式也可以實作深拷貝,而且這種方式還可以解決多層克隆的問題,多層克隆就是引用類型裡面又有引用類型,層層嵌套下去,用 Cloneable 方式實作還是比較麻煩的,一不小心寫錯了就不能實作深拷貝了,使用 Serializable 序列化的方式就需要所有的對象對實作 Serializable 接口,我們對代碼進行改造,改造成序列化的方式public class Person implements Serializable {

private static final long serialVersionUID = 369285298572941L;

public Person clone() {

Person person = null;

try { // 将該對象序列化成流,因為寫在流裡的是對象的一個拷貝,而原對象仍然存在于JVM裡面。是以利用這個特性可以實作對象的深拷貝

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) {

public class PersonDesc implements Serializable {

private static final long serialVersionUID = 872390113109L;

public String getDesc() {

return desc;

this.desc = desc;

}        運作 main 方法,我們可以得到跟 Cloneable 方式一樣的結果,序列化的方式也實作了深拷貝。到此關于 Java 淺拷貝和深拷貝的相關内容就介紹完了,希望你有所收獲。