這是今天我們在技術群里面討論的一個知識點,討論的相當激烈,由於對這一塊使用的比較少,所以對這一塊多少有些盲區。這篇文章總結了所討論的內容,希望這篇文章對你有所幫助。
在 Java 開發中,對象拷貝或者說對象克隆是常有的事,對象克隆最終都離不開直接賦值、淺拷貝、深拷貝 這三種方式,其中直接賦值應該是我們最常用的一種方式吧,對於淺拷貝和深拷貝可能用的少,所以或多或少存在一些誤區,這篇文章會詳細的介紹這三種對象克隆方式。
前置知識
值類型:Java 的基本數據類型,例如 int、float
引用類型:自定義類和 Java 包裝類(string、integer)
直接賦值
直接賦值是我們最常用的方式,在我們代碼中的體現是Persona = new Person();Person b = a
,是一種簡單明了的方式,但是它只是拷貝了對象引用地址而已,並沒有在內存中生成新的對象,我們可以通過下面這個例子來證明這一點
// 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,"123456@qq.com","我是張三");
// 復制對象
Person person1 = person;
// 改變 person1 的屬性值
person1.setName("我不是張三了");
System.out.println("person對象:"+person);
System.out.println("person1對象:"+person1);
}
}
運行上面代碼,你會得到如下結果:
person對象:Person{name='我不是張三了', age=20, email='123456@qq.com', desc='我是張三'}
person1對象:Person{name='我不是張三了', age=20, email='123456@qq.com', desc='我是張三'}
我們將 person 對象復制給了 person1 對象,我們對 person1 對象的 name 屬性進行了修改,並未修改 person 對象的name 屬性值,但是我們最后發現 person 對象的 name 屬性也發生了變化,其實不止這一個值,對於其他值也是一樣的,所以這結果證明了我們上面的結論:直接賦值的方式沒有生產新的對象,只是生新增了一個對象引用,直接賦值在 Java 內存中的模型大概是這樣的
淺拷貝
淺拷貝也可以實現對象克隆,從這名字你或許可以知道,這種拷貝一定存在某種缺陷,是的,它就是存在一定的缺陷,先來看看淺拷貝的定義:如果原型對象的成員變量是值類型,將復制一份給克隆對象,也就是說在堆中擁有獨立的空間;如果原型對象的成員變量是引用類型,則將引用對象的地址復制一份給克隆對象,也就是說原型對象和克隆對象的成員變量指向相同的內存地址。換句話說,在淺克隆中,當對象被復制時只復制它本身和其中包含的值類型的成員變量,而引用類型的成員對象並沒有復制。 可能你沒太理解這段話,那么我們在來看看淺拷貝的通用模型:
要實現對象淺拷貝還是比較簡單的,只需要被復制類需要實現 Cloneable 接口,重寫 clone 方法即可,對 person 類進行改造,使其可以支持淺拷貝。
public class Person implements Cloneable {
// 姓名
private String name;
// 年齡
private int age;
// 郵件
private String email;
// 描述
private String desc;
/*
* 重寫 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 person = new Person("張三",20,"123456@qq.com","我是張三");
// 復制對象
Person person1 = (Person) person.clone();
// 改變 person1 的屬性值
person1.setName("我是張三的克隆對象");
// 修改 person age 的值
person1.setAge(22);
System.out.println("person對象:"+person);
System.out.println();
System.out.println("person1對象:"+person1);
}
}
重新運行 main 方法,結果如下:
person對象:Person{name='張三', age=20, email='123456@qq.com', desc='我是張三'}
person1對象:Person{name='我是張三的克隆對象', age=22, email='123456@qq.com', 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 {
// 描述
private String desc;
}
public class Person implements Cloneable {
// 姓名
private String name;
// 年齡
private int age;
// 郵件
private String email;
// 將原來的 string desc 變成了 PersonDesc 對象,這樣 personDesc 就是引用類型
private PersonDesc personDesc;
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
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();
this.personDesc.setDesc(desc);
}
...省略...
}
修改 main 方法
public class PersonApp {
public static void main(String[] args) throws Exception {
// 初始化一個對象
Person person = new Person("平頭哥",20,"123456@qq.com","我的公眾號是:平頭哥的技術博文");
// 復制對象
Person person1 = (Person) person.clone();
// 改變 person1 的屬性值
person1.setName("我是平頭哥的克隆對象");
// 修改 person age 的值
person1.setAge(22);
person1.setDesc("我已經關注了平頭哥的技術博文公眾號");
System.out.println("person對象:"+person);
System.out.println();
System.out.println("person1對象:"+person1);
}
}
運行 main 方法,得到如下結果:
person對象:Person{name='平頭哥', age=20, email='123456@qq.com', desc='我已經關注了平頭哥的技術博文公眾號'}
person1對象:Person{name='我是平頭哥的克隆對象', age=22, email='123456@qq.com', desc='我已經關注了平頭哥的技術博文公眾號'}
我們修改 person1 的 desc 字段之后,person 的 desc 也發生了改變,這說明 person 對象和 person1 對象指向是同一個 PersonDesc 對象地址,這也符合淺拷貝引用對象只拷貝引用地址並未創建新對象的定義,到這你應該知道淺拷貝了吧。
深拷貝
深拷貝也是對象克隆的一種方式,相對於淺拷貝,深拷貝是一種完全拷貝,無論是值類型還是引用類型都會完完全全的拷貝一份,在內存中生成一個新的對象,簡單點說就是拷貝對象和被拷貝對象沒有任何關系,互不影響。深拷貝的通用模型如下:
深拷貝有兩種方式,一種是跟淺拷貝一樣實現 Cloneable 接口,另一種是實現 Serializable 接口,用序列化的方式來實現深拷貝,我們分別用這兩種方式來實現深拷貝
實現 Cloneable 接口方式
實現 Cloneable 接口的方式跟淺拷貝相差不大,我們需要引用對象也實現 Cloneable 接口,具體代碼改造如下:
public class PersonDesc implements Cloneable{
// 描述
private String desc;
...省略...
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
public class Person implements Cloneable {
// 姓名
private String name;
// 年齡
private int age;
// 郵件
private String email;
private PersonDesc personDesc;
/**
* clone 方法不是簡單的調用super的clone 就好,
*/
@Override
public Object clone() throws CloneNotSupportedException {
Person person = (Person)super.clone();
// 需要將引用對象也克隆一次
person.personDesc = (PersonDesc) personDesc.clone();
return person;
}
...省略...
}
main 方法不需要任何改動,我們再次運行 main 方法,得到如下結果:
person對象:Person{name='平頭哥', age=20, email='123456@qq.com', desc='我的公眾號是:平頭哥的技術博文'}
person1對象:Person{name='我是平頭哥的克隆對象', age=22, email='123456@qq.com', desc='我已經關注了平頭哥的技術博文公眾號'}
可以看出,修改 person1 的 desc 時對 person 的 desc 已經沒有影響了,說明進行了深拷貝,在內存中重新生成了一個新的對象。
實現 Serializable 接口方式
實現 Serializable 接口方式也可以實現深拷貝,而且這種方式還可以解決多層克隆的問題,多層克隆就是引用類型里面又有引用類型,層層嵌套下去,用 Cloneable 方式實現還是比較麻煩的,一不小心寫錯了就不能實現深拷貝了,使用 Serializable 序列化的方式就需要所有的對象對實現 Serializable 接口,我們對代碼進行改造,改造成序列化的方式
public class Person implements Serializable {
private static final long serialVersionUID = 369285298572941L;
// 姓名
private String name;
// 年齡
private int age;
// 郵件
private String email;
private PersonDesc personDesc;
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) {
e.printStackTrace();
}
return person;
}
public void setDesc(String desc) {
this.personDesc.setDesc(desc);
}
...省略...
}
public class PersonDesc implements Serializable {
private static final long serialVersionUID = 872390113109L;
// 描述
private String desc;
public String getDesc() {
return desc;
}
public void setDesc(String desc) {
this.desc = desc;
}
}
public class PersonApp {
public static void main(String[] args) throws Exception {
// 初始化一個對象
Person person = new Person("平頭哥",20,"123456@qq.com","我的公眾號是:平頭哥的技術博文");
// 復制對象
Person person1 = (Person) person.clone();
// 改變 person1 的屬性值
person1.setName("我是平頭哥的克隆對象");
// 修改 person age 的值
person1.setAge(22);
person1.setDesc("我已經關注了平頭哥的技術博文公眾號");
System.out.println("person對象:"+person);
System.out.println();
System.out.println("person1對象:"+person1);
}
}
運行 main 方法,我們可以得到跟 Cloneable 方式一樣的結果,序列化的方式也實現了深拷貝。到此關於 Java 淺拷貝和深拷貝的相關內容就介紹完了,希望你有所收獲。
最后
目前互聯網上很多大佬都有 Java 對象克隆文章,如有雷同,請多多包涵了。原創不易,碼字不易,還希望大家多多支持。若文中有所錯誤之處,還望提出,謝謝。
歡迎掃碼關注微信公眾號:「平頭哥的技術博文」,和平頭哥一起學習,一起進步。