Java 淺拷貝和深拷貝的理解和實現(xiàn)方式

點擊上方「Java有貨」關(guān)注我們

+
Java中的對象拷貝(Object Copy)指的是將一個對象的所有屬性(成員變量)拷貝到另一個有著相同類類型的對象中去。舉例說明:比如,對象A和對象B都屬于類S,具有屬性a和b。那么對對象A進(jìn)行拷貝操作賦值給對象B就是:B.a=A.a; B.b=A.b;在程序中拷貝對象是很常見的,主要是為了在新的上下文環(huán)境中復(fù)用現(xiàn)有對象的部分或全部 數(shù)據(jù)。
Java中的對象拷貝主要分為:淺拷貝(Shallow Copy)、深拷貝(Deep Copy)。
先介紹一點鋪墊知識:Java中的數(shù)據(jù)類型分為基本數(shù)據(jù)類型和引用數(shù)據(jù)類型。對于這兩種數(shù)據(jù)類型,在進(jìn)行賦值操作、用作方法參數(shù)或返回值時,會有值傳遞和引用(地址)傳遞的差別。
淺拷貝(Shallow Copy)
①對于數(shù)據(jù)類型是基本數(shù)據(jù)類型的成員變量,淺拷貝會直接進(jìn)行值傳遞,也就是將該屬性值復(fù)制一份給新的對象。因為是兩份不同的數(shù)據(jù),所以對其中一個對象的該成員變量值進(jìn)行修改,不會影響另一個對象拷貝得到的數(shù)據(jù)。
②對于數(shù)據(jù)類型是引用數(shù)據(jù)類型的成員變量,比如說成員變量是某個數(shù)組、某個類的對象等,那么淺拷貝會進(jìn)行引用傳遞,也就是只是將該成員變量的引用值(內(nèi)存地址)復(fù)制一份給新的對象。因為實際上兩個對象的該成員變量都指向同一個實例。在這種情況下,在一個對象中修改該成員變量會影響到另一個對象的該成員變量值。
具體模型如圖所示:可以看到基本數(shù)據(jù)類型的成員變量,對其值創(chuàng)建了新的拷貝。而引用數(shù)據(jù)類型的成員變量的實例仍然是只有一份,兩個對象的該成員變量都指向同一個實例。

淺拷貝的實現(xiàn)方式
通過拷貝構(gòu)造方法實現(xiàn)淺拷貝:
拷貝構(gòu)造方法指的是該類的構(gòu)造方法參數(shù)為該類的對象。使用拷貝構(gòu)造方法可以很好地完成淺拷貝,直接通過一個現(xiàn)有的對象創(chuàng)建出與該對象屬性相同的新的對象。
代碼參考如下:
/* 拷貝構(gòu)造方法實現(xiàn)淺拷貝 */
public class CopyConstructor {
public static void main(String[] args) {
Age a=new Age(20);
Person p1=new Person(a,"搖頭耶穌");
Person p2=new Person(p1);
System.out.println("p1是"+p1);
System.out.println("p2是"+p2);
//修改p1的各屬性值,觀察p2的各屬性值是否跟隨變化
p1.setName("小傻瓜");
a.setAge(99);
System.out.println("修改后的p1是"+p1);
System.out.println("修改后的p2是"+p2);
}
}
class Person{
//兩個屬性值:分別代表值傳遞和引用傳遞
private Age age;
private String name;
public Person(Age age,String name) {
this.age=age;
this.name=name;
}
//拷貝構(gòu)造方法
public Person(Person p) {
this.name=p.name;
this.age=p.age;
}
public void setName(String name) {
this.name=name;
}
public String toString() {
return this.name+" "+this.age;
}
}
class Age{
private int age;
public Age(int age) {
this.age=age;
}
//get set
}
運(yùn)行結(jié)果為:
p1是搖頭耶穌 20
p2是搖頭耶穌 20修改后的
p1是小傻瓜 99修改后的
p2是搖頭耶穌 99
結(jié)果分析:這里對Person類選擇了兩個具有代表性的屬性值:一個是引用傳遞類型;另一個是字符串類型(屬于常量)。
通過拷貝構(gòu)造方法進(jìn)行了淺拷貝,各屬性值成功復(fù)制。其中,p1值傳遞部分的屬性值發(fā)生變化時,p2不會隨之改變;而引用傳遞部分屬性值發(fā)生變化時,p2也隨之改變。
要注意:如果在拷貝構(gòu)造方法中,對引用數(shù)據(jù)類型變量逐一開辟新的內(nèi)存空間,創(chuàng)建新的對象,也可以實現(xiàn)深拷貝。而對于一般的拷貝構(gòu)造,則一定是淺拷貝。
通過重寫clone()方法進(jìn)行淺拷貝:
Object類是類結(jié)構(gòu)的根類,其中有一個方法為protected Object clone() throws CloneNotSupportedException,這個方法就是進(jìn)行的淺拷貝。有了這個淺拷貝模板,我們可以通過調(diào)用clone()方法來實現(xiàn)對象的淺拷貝。但是需要注意:1、Object類雖然有這個方法,但是這個方法是受保護(hù)的(被protected修飾),所以我們無法直接使用。2、使用clone方法的類必須實現(xiàn)Cloneable接口,否則會拋出異常CloneNotSupportedException。對于這兩點,我們的解決方法是,在要使用clone方法的類中重寫clone()方法,通過super.clone()調(diào)用Object類中的原clone方法。
參考代碼如下:對Student類的對象進(jìn)行拷貝,直接重寫clone()方法,通過調(diào)用clone方法即可完成淺拷貝。
/* clone方法實現(xiàn)淺拷貝 */
public class ShallowCopy {
public static void main(String[] args) {
Age a=new Age(20);
Student stu1=new Student("搖頭耶穌",a,175);
//通過調(diào)用重寫后的clone方法進(jìn)行淺拷貝
Student stu2=(Student)stu1.clone();
System.out.println(stu1.toString());
System.out.println(stu2.toString());
//嘗試修改stu1中的各屬性,觀察stu2的屬性有沒有變化
stu1.setName("大傻子");
//改變age這個引用類型的成員變量的值
a.setAge(99);
//stu1.setaAge(new Age(99)); 使用這種方式修改age屬性值的話,stu2是不會跟著改變的。因為創(chuàng)建了一個新的Age類對象而不是改變原對象的實例值
stu1.setLength(216);
System.out.println(stu1.toString());
System.out.println(stu2.toString());
}
}
/*
* 創(chuàng)建年齡類
*/
class Age{
//年齡類的成員變量(屬性)
private int age;
//構(gòu)造方法
public Age(int age) {
this.age=age;
}
//get set
}
/*
* 創(chuàng)建學(xué)生類
*/
class Student implements Cloneable{
//學(xué)生類的成員變量(屬性),其中一個屬性為類的對象
private String name;
private Age aage;
private int length;
//構(gòu)造方法,其中一個參數(shù)為另一個類的對象
public Student(String name,Age a,int length) {
this.name=name;
this.aage=a;
this.length=length;
}
//get set
//設(shè)置輸出的字符串形式
public String toString() {
return "姓名是:"+this.getName()+", 年齡為:"+this.getaAge().toString()+", 長度是:"+this.getLength();
}
//重寫Object類的clone方法
public Object clone() {
Object obj=null;
//調(diào)用Object類的clone方法,返回一個Object實例
try {
obj= super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return obj;
}
}
運(yùn)行結(jié)果如下:
姓名是:搖頭耶穌, 年齡為:20, 長度是:175
姓名是:搖頭耶穌, 年齡為:20, 長度是:175
姓名是:大傻子, 年齡為:99, 長度是:216
姓名是:搖頭耶穌, 年齡為:99, 長度是:175
其中:Student類的成員變量我有代表性地設(shè)置了三種:基本數(shù)據(jù)類型的成員變量length,引用數(shù)據(jù)類型的成員變量aage和字符串String類型的name.
分析結(jié)果可以驗證:
基本數(shù)據(jù)類型是值傳遞,所以修改值后不會影響另一個對象的該屬性值;
引用數(shù)據(jù)類型是地址傳遞(引用傳遞),所以修改值后另一個對象的該屬性值會同步被修改。
String類型非常特殊,所以我額外設(shè)置了一個字符串類型的成員變量來進(jìn)行說明。首先,String類型屬于引用數(shù)據(jù)類型,不屬于基本數(shù)據(jù)類型,但是String類型的數(shù)據(jù)是存放在常量池中的,也就是無法修改的!也就是說,當(dāng)我將name屬性從“搖頭耶穌”改為“大傻子"后,并不是修改了這個數(shù)據(jù)的值,而是把這個數(shù)據(jù)的引用從指向”搖頭耶穌“這個常量改為了指向”大傻子“這個常量。在這種情況下,另一個對象的name屬性值仍然指向”搖頭耶穌“不會受到影響。
深拷貝
首先介紹對象圖的概念。設(shè)想一下,一個類有一個對象,其成員變量中又有一個對象,該對象指向另一個對象,另一個對象又指向另一個對象,直到一個確定的實例。這就形成了對象圖。那么,對于深拷貝來說,不僅要復(fù)制對象的所有基本數(shù)據(jù)類型的成員變量值,還要為所有引用數(shù)據(jù)類型的成員變量申請存儲空間,并復(fù)制每個引用數(shù)據(jù)類型成員變量所引用的對象,直到該對象可達(dá)的所有對象。也就是說,對象進(jìn)行深拷貝要對整個對象圖進(jìn)行拷貝!
簡單地說,深拷貝對引用數(shù)據(jù)類型的成員變量的對象圖中所有的對象都開辟了內(nèi)存空間;而淺拷貝只是傳遞地址指向,新的對象并沒有對引用數(shù)據(jù)類型創(chuàng)建內(nèi)存空間。
深拷貝模型如圖所示:可以看到所有的成員變量都進(jìn)行了復(fù)制。

因為創(chuàng)建內(nèi)存空間和拷貝整個對象圖,所以深拷貝相比于淺拷貝速度較慢并且花銷較大。
深拷貝的實現(xiàn)方法主要有兩種:
通過重寫clone方法來實現(xiàn)深拷貝
與通過重寫clone方法實現(xiàn)淺拷貝的基本思路一樣,只需要為對象圖的每一層的每一個對象都實現(xiàn)Cloneable接口并重寫clone方法,最后在最頂層的類的重寫的clone方法中調(diào)用所有的clone方法即可實現(xiàn)深拷貝。簡單的說就是:每一層的每個對象都進(jìn)行淺拷貝=深拷貝。
參考代碼如下:
package linearList;
/* 層次調(diào)用clone方法實現(xiàn)深拷貝 */
public class DeepCopy {
public static void main(String[] args) {
Age a=new Age(20);
Student stu1=new Student("搖頭耶穌",a,175);
//通過調(diào)用重寫后的clone方法進(jìn)行淺拷貝
Student stu2=(Student)stu1.clone();
System.out.println(stu1.toString());
System.out.println(stu2.toString());
System.out.println();
//嘗試修改stu1中的各屬性,觀察stu2的屬性有沒有變化
stu1.setName("大傻子");
//改變age這個引用類型的成員變量的值
a.setAge(99);
//stu1.setaAge(new Age(99)); 使用這種方式修改age屬性值的話,stu2是不會跟著改變的。因為創(chuàng)建了一個新的Age類對象而不是改變原對象的實例值
stu1.setLength(216);
System.out.println(stu1.toString());
System.out.println(stu2.toString());
}
}
/*
* 創(chuàng)建年齡類
*/
class Age implements Cloneable{
//年齡類的成員變量(屬性)
private int age;
//構(gòu)造方法
public Age(int age) {
this.age=age;
}
// get set
//重寫Object的clone方法
public Object clone() {
Object obj=null;
try {
obj=super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return obj;
}
}
/*
* 創(chuàng)建學(xué)生類
*/
class Student implements Cloneable{
//學(xué)生類的成員變量(屬性),其中一個屬性為類的對象
private String name;
private Age aage;
private int length;
//構(gòu)造方法,其中一個參數(shù)為另一個類的對象
public Student(String name,Age a,int length) {
this.name=name;
this.aage=a;
this.length=length;
}
// get set
public String toString() {
return "姓名是:"+this.getName()+", 年齡為:"+this.getaAge().toString()+", 長度是:"+this.getLength();
}
//重寫Object類的clone方法
public Object clone() {
Object obj=null;
//調(diào)用Object類的clone方法——淺拷貝
try {
obj= super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
//調(diào)用Age類的clone方法進(jìn)行深拷貝
//先將obj轉(zhuǎn)化為學(xué)生類實例
Student stu=(Student)obj;
//學(xué)生類實例的Age對象屬性,調(diào)用其clone方法進(jìn)行拷貝
stu.aage=(Age)stu.getaAge().clone();
return obj;
}
}
運(yùn)行結(jié)果如下:
姓名是:搖頭耶穌, 年齡為:20, 長度是:175
姓名是:搖頭耶穌, 年齡為:20, 長度是:175
姓名是:大傻子, 年齡為:99, 長度是:216
姓名是:搖頭耶穌, 年齡為:20, 長度是:175
分析結(jié)果可以驗證:進(jìn)行了深拷貝之后,無論是什么類型的屬性值的修改,都不會影響另一個對象的屬性值。
通過對象序列化實現(xiàn)深拷貝
雖然層次調(diào)用clone方法可以實現(xiàn)深拷貝,但是顯然代碼量實在太大。特別對于屬性數(shù)量比較多、層次比較深的類而言,每個類都要重寫clone方法太過繁瑣。
將對象序列化為字節(jié)序列后,默認(rèn)會將該對象的整個對象圖進(jìn)行序列化,再通過反序列即可完美地實現(xiàn)深拷貝。
參考代碼如下:
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
/* 通過序列化實現(xiàn)深拷貝 */
public class DeepCopyBySerialization {
public static void main(String[] args) throws IOException, ClassNotFoundException {
Age a=new Age(20);
Student stu1=new Student("搖頭耶穌",a,175);
//通過序列化方法實現(xiàn)深拷貝
ByteArrayOutputStream bos=new ByteArrayOutputStream();
ObjectOutputStream oos=new ObjectOutputStream(bos);
oos.writeObject(stu1);
oos.flush();
ObjectInputStream ois=new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()));
Student stu2=(Student)ois.readObject();
System.out.println(stu1.toString());
System.out.println(stu2.toString());
System.out.println();
//嘗試修改stu1中的各屬性,觀察stu2的屬性有沒有變化
stu1.setName("大傻子");
//改變age這個引用類型的成員變量的值
a.setAge(99);
stu1.setLength(216);
System.out.println(stu1.toString());
System.out.println(stu2.toString());
}
}
/*
* 創(chuàng)建年齡類
*/
class Age implements Serializable{
//年齡類的成員變量(屬性)
private int age;
//構(gòu)造方法
public Age(int age) {
this.age=age;
}
}
/*
* 創(chuàng)建學(xué)生類
*/
class Student implements Serializable{
//學(xué)生類的成員變量(屬性),其中一個屬性為類的對象
private String name;
private Age aage;
private int length;
//構(gòu)造方法,其中一個參數(shù)為另一個類的對象
public Student(String name,Age a,int length) {
this.name=name;
this.aage=a;
this.length=length;
}
//get set....
//設(shè)置輸出的字符串形式
public String toString() {
return "姓名是:"+this.getName()+", 年齡為:"+this.getaAge().toString()+", 長度是:"+this.getLength();
}
}運(yùn)行結(jié)果為:
姓名是:搖頭耶穌, 年齡為:20, 長度是:175
姓名是:搖頭耶穌, 年齡為:20, 長度是:175
姓名是:大傻子, 年齡為:99, 長度是:216
姓名是:搖頭耶穌, 年齡為:20, 長度是:175
可以通過很簡潔的代碼即可完美實現(xiàn)深拷貝。不過要注意的是,如果某個屬性被transient修飾,那么該屬性就無法被拷貝了。 以上是淺拷貝的深拷貝的區(qū)別和實現(xiàn)方式。
