這是今天我們在技術群裡面讨論的一個 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 記憶體中的模型大概是這樣的
淺拷貝 淺拷貝也可以實作對象克隆,從這名字你或許可以知道,這種拷貝一定存在某種缺陷,是的,它就是存在一定的缺陷,先來看看淺拷貝的定義:如果原型對象的成員變量是值類型,将複制一份給克隆對象,也就是說在堆中擁有獨立的空間;如果原型對象的成員變量是引用類型,則将引用對象的位址複制一份給克隆對象,也就是說原型對象和克隆對象的成員變量指向相同的記憶體位址。換句話說,在淺克隆中,當對象被複制時隻複制它本身和其中包含的值類型的成員變量,而引用類型的成員對象并沒有複制。可能你沒太了解這段話,那麼我們在來看看淺拷貝的通用模型:
淺拷貝通用模型 要實作對象淺拷貝還是比較簡單的,隻需要被複制類需要實作 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 對象位址,這也符合淺拷貝引用對象隻拷貝引用位址并未建立新對象的定義,到這你應該知道淺拷貝了吧。深拷貝
深拷貝也是對象克隆的一種方式,相對于淺拷貝,深拷貝是一種完全拷貝,無論是值類型還是引用類型都會完完全全的拷貝一份,在記憶體中生成一個新的對象,簡單點說就是拷貝對象和被拷貝對象沒有任何關系,互不影響。深拷貝的通用模型如下:
深拷貝通用模型 深拷貝有兩種方式,一種是跟淺拷貝一樣實作 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 淺拷貝和深拷貝的相關内容就介紹完了,希望你有所收獲。