轉載:https://www.cnblogs.com/shakinghead/p/7651502.html
Java中的對象拷貝(Object Copy)指的是將一個對象的所有屬性(成員變量)拷貝到另一個有着相同類類型的對象中去。舉例說明:比如,對象A和對象B都屬於類S,具有屬性a和b。那么對對象A進行拷貝操作賦值給對象B就是:B.a=A.a; B.b=A.b;
在程序中拷貝對象是很常見的,主要是為了在新的上下文環境中復用現有對象的部分或全部 數據。
Java中的對象拷貝主要分為:淺拷貝(Shallow Copy)、深拷貝(Deep Copy)。
先介紹一點鋪墊知識:Java中的數據類型分為基本數據類型和引用數據類型。對於這兩種數據類型,在進行賦值操作、用作方法參數或返回值時,會有值傳遞和引用(地址)傳遞的差別。
淺拷貝(Shallow Copy):①對於數據類型是基本數據類型的成員變量,淺拷貝會直接進行值傳遞,也就是將該屬性值復制一份給新的對象。因為是兩份不同的數據,所以對其中一個對象的該成員變量值進行修改,不會影響另一個對象拷貝得到的數據。②對於數據類型是引用數據類型的成員變量,比如說成員變量是某個數組、某個類的對象等,那么淺拷貝會進行引用傳遞,也就是只是將該成員變量的引用值(內存地址)復制一份給新的對象。因為實際上兩個對象的該成員變量都指向同一個實例。在這種情況下,在一個對象中修改該成員變量會影響到另一個對象的該成員變量值。
具體模型如圖所示:可以看到基本數據類型的成員變量,對其值創建了新的拷貝。而引用數據類型的成員變量的實例仍然是只有一份,兩個對象的該成員變量都指向同一個實例。
淺拷貝的實現方式主要有三種:
一、通過拷貝構造方法實現淺拷貝:
/* 拷貝構造方法實現淺拷貝 */ 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; } //拷貝構造方法 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; } public void setAge(int age) { this.age=age; } public int getAge() { return this.age; } public String toString() { return getAge()+""; } }
運行結果為:
p1是搖頭耶穌 20
p2是搖頭耶穌 20
修改后的p1是小傻瓜 99
修改后的p2是搖頭耶穌 99
結果分析:這里對Person類選擇了兩個具有代表性的屬性值:一個是引用傳遞類型;另一個是字符串類型(屬於常量)。
通過拷貝構造方法進行了淺拷貝,各屬性值成功復制。其中,p1值傳遞部分的屬性值發生變化時,p2不會隨之改變;而引用傳遞部分屬性值發生變化時,p2也隨之改變。
要注意:如果在拷貝構造方法中,對引用數據類型變量逐一開辟新的內存空間,創建新的對象,也可以實現深拷貝。而對於一般的拷貝構造,則一定是淺拷貝。
二、通過重寫clone()方法進行淺拷貝:
Object類是類結構的根類,其中有一個方法為protected Object clone() throws CloneNotSupportedException,這個方法就是進行的淺拷貝。有了這個淺拷貝模板,我們可以通過調用clone()方法來實現對象的淺拷貝。但是需要注意:1、Object類雖然有這個方法,但是這個方法是受保護的(被protected修飾),所以我們無法直接使用。2、使用clone方法的類必須實現Cloneable接口,否則會拋出異常CloneNotSupportedException。對於這兩點,我們的解決方法是,在要使用clone方法的類中重寫clone()方法,通過super.clone()調用Object類中的原clone方法。
參考代碼如下:對Student類的對象進行拷貝,直接重寫clone()方法,通過調用clone方法即可完成淺拷貝。
使用拷貝構造方法可以很好地完成淺拷貝,直接通過一個現有的對象創建出與該對象屬性相同的新的對象。
/* clone方法實現淺拷貝 */ public class ShallowCopy { public static void main(String[] args) { Age a=new Age(20); Student stu1=new Student("搖頭耶穌",a,175); //通過調用重寫后的clone方法進行淺拷貝 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是不會跟着改變的。因為創建了一個新的Age類對象而不是改變原對象的實例值 stu1.setLength(216); System.out.println(stu1.toString()); System.out.println(stu2.toString()); } } /* * 創建年齡類 */ class Age{ //年齡類的成員變量(屬性) private int age; //構造方法 public Age(int age) { this.age=age; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } public String toString() { return this.age+""; } } /* * 創建學生類 */ class Student implements Cloneable{ //學生類的成員變量(屬性),其中一個屬性為類的對象 private String name; private Age aage; private int length; //構造方法,其中一個參數為另一個類的對象 public Student(String name,Age a,int length) { this.name=name; this.aage=a; this.length=length; } //eclipe中alt+shift+s自動添加所有的set和get方法 public String getName() { return name; } public void setName(String name) { this.name = name; } public Age getaAge() { return this.aage; } public void setaAge(Age age) { this.aage=age; } public int getLength() { return this.length; } public void setLength(int length) { this.length=length; } //設置輸出的字符串形式 public String toString() { return "姓名是: "+this.getName()+", 年齡為: "+this.getaAge().toString()+", 長度是: "+this.getLength(); } //重寫Object類的clone方法 public Object clone() { Object obj=null; //調用Object類的clone方法,返回一個Object實例 try { obj= super.clone(); } catch (CloneNotSupportedException e) { e.printStackTrace(); } return obj; } }
運行結果如下:
姓名是: 搖頭耶穌, 年齡為: 20, 長度是: 175
姓名是: 搖頭耶穌, 年齡為: 20, 長度是: 175
姓名是: 大傻子, 年齡為: 99, 長度是: 216
姓名是: 搖頭耶穌, 年齡為: 99, 長度是: 175
其中:Student類的成員變量我有代表性地設置了三種:基本數據類型的成員變量length,引用數據類型的成員變量aage和字符串String類型的name.
分析結果可以驗證:
基本數據類型是值傳遞,所以修改值后不會影響另一個對象的該屬性值;
引用數據類型是地址傳遞(引用傳遞),所以修改值后另一個對象的該屬性值會同步被修改。
String類型非常特殊,所以我額外設置了一個字符串類型的成員變量來進行說明。首先,String類型屬於引用數據類型,不屬於基本數據類型,但是String類型的數據是存放在常量池中的,也就是無法修改的!也就是說,當我將name屬性從“搖頭耶穌”改為“大傻子"后,並不是修改了這個數據的值,而是把這個數據的引用從指向”搖頭耶穌“這個常量改為了指向”大傻子“這個常量。在這種情況下,另一個對象的name屬性值仍然指向”搖頭耶穌“不會受到影響。
深拷貝:首先介紹對象圖的概念。設想一下,一個類有一個對象,其成員變量中又有一個對象,該對象指向另一個對象,另一個對象又指向另一個對象,直到一個確定的實例。這就形成了對象圖。那么,對於深拷貝來說,不僅要復制對象的所有基本數據類型的成員變量值,還要為所有引用數據類型的成員變量申請存儲空間,並復制每個引用數據類型成員變量所引用的對象,直到該對象可達的所有對象。也就是說,對象進行深拷貝要對整個對象圖進行拷貝!
簡單地說,深拷貝對引用數據類型的成員變量的對象圖中所有的對象都開辟了內存空間;而淺拷貝只是傳遞地址指向,新的對象並沒有對引用數據類型創建內存空間。
深拷貝模型如圖所示:可以看到所有的成員變量都進行了復制。
因為創建內存空間和拷貝整個對象圖,所以深拷貝相比於淺拷貝速度較慢並且花銷較大。
深拷貝的實現方法主要有兩種:
一、通過重寫clone方法來實現深拷貝
與通過重寫clone方法實現淺拷貝的基本思路一樣,只需要為對象圖的每一層的每一個對象都實現Cloneable接口並重寫clone方法,最后在最頂層的類的重寫的clone方法中調用所有的clone方法即可實現深拷貝。簡單的說就是:每一層的每個對象都進行淺拷貝=深拷貝。
package linearList; /* 層次調用clone方法實現深拷貝 */ public class DeepCopy { public static void main(String[] args) { Age a=new Age(20); Student stu1=new Student("搖頭耶穌",a,175); //通過調用重寫后的clone方法進行淺拷貝 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是不會跟着改變的。因為創建了一個新的Age類對象而不是改變原對象的實例值 stu1.setLength(216); System.out.println(stu1.toString()); System.out.println(stu2.toString()); } } /* * 創建年齡類 */ class Age implements Cloneable{ //年齡類的成員變量(屬性) private int age; //構造方法 public Age(int age) { this.age=age; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } public String toString() { return this.age+""; } //重寫Object的clone方法 public Object clone() { Object obj=null; try { obj=super.clone(); } catch (CloneNotSupportedException e) { e.printStackTrace(); } return obj; } } /* * 創建學生類 */ class Student implements Cloneable{ //學生類的成員變量(屬性),其中一個屬性為類的對象 private String name; private Age aage; private int length; //構造方法,其中一個參數為另一個類的對象 public Student(String name,Age a,int length) { this.name=name; this.aage=a; this.length=length; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Age getaAge() { return this.aage; } public void setaAge(Age age) { this.aage=age; } public int getLength() { return this.length; } public void setLength(int length) { this.length=length; } public String toString() { return "姓名是: "+this.getName()+", 年齡為: "+this.getaAge().toString()+", 長度是: "+this.getLength(); } //重寫Object類的clone方法 public Object clone() { Object obj=null; //調用Object類的clone方法——淺拷貝 try { obj= super.clone(); } catch (CloneNotSupportedException e) { e.printStackTrace(); } //調用Age類的clone方法進行深拷貝 //先將obj轉化為學生類實例 Student stu=(Student)obj; //學生類實例的Age對象屬性,調用其clone方法進行拷貝 stu.aage=(Age)stu.getaAge().clone(); return obj; } }
姓名是: 搖頭耶穌, 年齡為: 20, 長度是: 175
姓名是: 搖頭耶穌, 年齡為: 20, 長度是: 175
姓名是: 大傻子, 年齡為: 99, 長度是: 216
姓名是: 搖頭耶穌, 年齡為: 20, 長度是: 175
分析結果可以驗證:進行了深拷貝之后,無論是什么類型的屬性值的修改,都不會影響另一個對象的屬性值。
二、通過對象序列化實現深拷貝
雖然層次調用clone方法可以實現深拷貝,但是顯然代碼量實在太大。特別對於屬性數量比較多、層次比較深的類而言,每個類都要重寫clone方法太過繁瑣。
將對象序列化為字節序列后,默認會將該對象的整個對象圖進行序列化,再通過反序列即可完美地實現深拷貝。
參考代碼如下:
import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; /* 通過序列化實現深拷貝 */ public class DeepCopyBySerialization { public static void main(String[] args) throws IOException, ClassNotFoundException { Age a=new Age(20); Student stu1=new Student("搖頭耶穌",a,175); //通過序列化方法實現深拷貝 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()); } } /* * 創建年齡類 */ class Age implements Serializable{ //年齡類的成員變量(屬性) private int age; //構造方法 public Age(int age) { this.age=age; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } public String toString() { return this.age+""; } } /* * 創建學生類 */ class Student implements Serializable{ //學生類的成員變量(屬性),其中一個屬性為類的對象 private String name; private Age aage; private int length; //構造方法,其中一個參數為另一個類的對象 public Student(String name,Age a,int length) { this.name=name; this.aage=a; this.length=length; } //eclipe中alt+shift+s自動添加所有的set和get方法 public String getName() { return name; } public void setName(String name) { this.name = name; } public Age getaAge() { return this.aage; } public void setaAge(Age age) { this.aage=age; } public int getLength() { return this.length; } public void setLength(int length) { this.length=length; } //設置輸出的字符串形式 public String toString() { return "姓名是: "+this.getName()+", 年齡為: "+this.getaAge().toString()+", 長度是: "+this.getLength(); } }
運行結果為:
姓名是: 搖頭耶穌, 年齡為: 20, 長度是: 175
姓名是: 搖頭耶穌, 年齡為: 20, 長度是: 175
姓名是: 大傻子, 年齡為: 99, 長度是: 216
姓名是: 搖頭耶穌, 年齡為: 20, 長度是: 175
可以通過很簡潔的代碼即可完美實現深拷貝。不過要注意的是,如果某個屬性被transient修飾,那么該屬性就無法被拷貝了。
以上是淺拷貝的深拷貝的區別和實現方式。