Java中的對象拷貝(Object Copy)指的是將一個對象的全部屬性(成員變量)拷貝到另外一個有着相同類類型的對象中去。舉例說明:好比,對象A和對象B都屬於類S,具備屬性a和b。那麼對對象A進行拷貝操做賦值給對象B就是:B.a=A.a; B.b=A.b;html
在程序中拷貝對象是很常見的,主要是爲了在新的上下文環境中複用現有對象的部分或所有 數據。java
Java中的對象拷貝主要分爲:淺拷貝(Shallow Copy)、深拷貝(Deep Copy)。api
先介紹一點鋪墊知識:Java中的數據類型分爲基本數據類型和引用數據類型。對於這兩種數據類型,在進行賦值操做、用做方法參數或返回值時,會有值傳遞和引用(地址)傳遞的差異。數組
淺拷貝(Shallow Copy):①對於數據類型是基本數據類型的成員變量,淺拷貝會直接進行值傳遞,也就是將該屬性值複製一份給新的對象。由於是兩份不一樣的數據,因此對其中一個對象的該成員變量值進行修改,不會影響另外一個對象拷貝獲得的數據。②對於數據類型是引用數據類型的成員變量,好比說成員變量是某個數組、某個類的對象等,那麼淺拷貝會進行引用傳遞,也就是隻是將該成員變量的引用值(內存地址)複製一份給新的對象。由於實際上兩個對象的該成員變量都指向同一個實例。在這種狀況下,在一個對象中修改該成員變量會影響到另外一個對象的該成員變量值。this
具體模型如圖所示:能夠看到基本數據類型的成員變量,對其值建立了新的拷貝。而引用數據類型的成員變量的實例仍然是隻有一份,兩個對象的該成員變量都指向同一個實例。spa
淺拷貝的實現方式主要有三種:.net
1、經過拷貝構造方法實現淺拷貝:code
拷貝構造方法指的是該類的構造方法參數爲該類的對象。使用拷貝構造方法能夠很好地完成淺拷貝,直接經過一個現有的對象建立出與該對象屬性相同的新的對象。htm
代碼參考以下:對象
/* 拷貝構造方法實現淺拷貝 */ 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也隨之改變。
要注意:若是在拷貝構造方法中,對引用數據類型變量逐一開闢新的內存空間,建立新的對象,也能夠實現深拷貝。而對於通常的拷貝構造,則必定是淺拷貝。
2、經過重寫clone()方法進行淺拷貝:
Object類是類結構的根類,其中有一個方法爲protected Object clone() throws CloneNotSupportedException,這個方法就是進行的淺拷貝。有了這個淺拷貝模板,咱們能夠經過調用clone()方法來實現對象的淺拷貝。可是須要注意:一、Object類雖然有這個方法,可是這個方法是受保護的(被protected修飾),因此咱們沒法直接使用。二、使用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屬性值仍然指向」搖頭耶穌「不會受到影響。
深拷貝:首先介紹對象圖的概念。設想一下,一個類有一個對象,其成員變量中又有一個對象,該對象指向另外一個對象,另外一個對象又指向另外一個對象,直到一個肯定的實例。這就造成了對象圖。那麼,對於深拷貝來講,不只要複製對象的全部基本數據類型的成員變量值,還要爲全部引用數據類型的成員變量申請存儲空間,並複製每一個引用數據類型成員變量所引用的對象,直到該對象可達的全部對象。也就是說,對象進行深拷貝要對整個對象圖進行拷貝!
簡單地說,深拷貝對引用數據類型的成員變量的對象圖中全部的對象都開闢了內存空間;而淺拷貝只是傳遞地址指向,新的對象並無對引用數據類型建立內存空間。
深拷貝模型如圖所示:能夠看到全部的成員變量都進行了複製。
由於建立內存空間和拷貝整個對象圖,因此深拷貝相比於淺拷貝速度較慢而且花銷較大。
深拷貝的實現方法主要有兩種:
1、經過重寫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
分析結果能夠驗證:進行了深拷貝以後,不管是什麼類型的屬性值的修改,都不會影響另外一個對象的屬性值。
2、經過對象序列化實現深拷貝
雖然層次調用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修飾,那麼該屬性就沒法被拷貝了。
以上是淺拷貝的深拷貝的區別和實現方式。
over.