作者 | sowhat1412 責編 | 張文
頭圖 | CSDN 下載自視覺中國
本文主要講解:對象創建方式、Java中的深拷貝和淺拷貝。
創建對象的5種方式
1.1 通過 new 關鍵字
這是最常用的一種方式,通過 new 關鍵字調用類的有參或無參構造方法來創建對象。比如 Object obj = new Object()。
1.2 通過 Class 類的 newInstance() 方法
這種默認是調用類的無參構造方法創建對象。比如:
Person p2 = (Person) Class. forName("com.ys.test. Person"). newInstance();
1.3 通過 Constructor 類的 newInstance 方法
這和第二種方法類時,都是通過反射來實現。通過 java.lang.relect.Constructor 類的 newInstance() 方法指定某個構造器來創建對象。實際上第二種方法利用 Class 的 newInstance() 方法創建對象,其內部調用還是 Constructor 的 newInstance() 方法。
Person p3 = (Person) Person.class.getConstructors()[0].newInstance();
1.4 利用 Clone 方法
Clone 是 Object 類中的一個方法,通過對象A.clone() 方法會創建一個內容和對象 A 一模一樣的對象 B,clone 克隆,顧名思義就是創建一個一模一樣的對象出來。
Person p4 = (Person) p3.clone();
1.5 序列化
序列化是把堆內存中的 Java 對象數據,通過某種方式把對象存儲到磁碟文件中或者傳遞給其他網絡節點(在網絡上傳輸)。而反序列化則是把磁碟文件中的對象數據或者把網絡節點上的對象數據,恢復成Java對象模型的過程。
Java 基本複製方法
java賦值是複製對象引用,如果我們想要得到一個對象的==副本==,使用賦值操作是無法達到目的的:修改新對象的值會同時修改舊對象的值。
public class Client{ public static void main(String[] args) throws CloneNotSupportedException { Person person = new Person(15, "sowhat", new Address("河北", "建華南大街")); Person p1 = person; p1.setAge(45); System.out.println(p1.hashCode()); System.out.println(person.hashCode()); System.out.println("================"); System.out.println(p1.display()); System.out.println(person.display()); }}
Clone 方法
如果創建一個對象的新的副本,也就是說他們的初始狀態完全一樣,但以後可以改變各自的狀態,而互不影響,就需要用到java中對象的複製,如原生的clone()方法。本次講解的是 Java 的深拷貝和淺拷貝,其實現方式正是通過調用 Object 類的 clone() 方法來完成。在 Object.class 類中,源碼為:
/** * ... * performs a "shallow copy" of this object, not a "deep copy" operation. * 上面這裡已經說明了,clone()方法是淺拷貝,而不是深拷貝 * @see java.lang.Cloneable */ protected native Object clone() throws CloneNotSupportedException;
這是一個用 native 關鍵字修飾的方法。關於native關鍵字有一篇博客專門有介紹。不理解也沒關係,只需要知道用 native 修飾的方法就是告訴作業系統。這個方法我不實現了,讓作業系統去實現(參考JNI)。具體怎麼實現我們不需要了解,只需要知道clone方法的作用就是複製對象,產生一個新的對象。那麼這個新的對象和原對象是==什麼關係呢==?
基本類型和引用類型
這裡再給大家普及一個概念,在 Java 中基本類型和引用類型的區別。在 Java 中數據類型可以分為兩大類:基本類型和引用類型。
基本類型也稱為值類型,分別是字符類型 char,布爾類型 boolean以及數值類型 byte、short、int、long、float、double。
引用類型則包括類、接口、數組、枚舉等。
Java 將內存空間分為堆和棧。基本類型直接在棧 stack中存儲數值,而引用類型是將引用放在棧中,實際存儲的值是放在堆 heap中,通過棧中的引用指向堆中存放的數據。
上圖定義的 a 和 b 都是基本類型,其值是直接存放在棧中的;而 c 和 d 是 String 聲明的,這是一個引用類型,引用地址是存放在棧中,然後指向堆的內存空間。下面 d = c,這條語句表示將 c 的引用賦值給 d,那麼 c 和 d 將指向同一塊堆內存空間。
淺拷貝
接下來用代碼看看淺拷貝的效果。
package mytest;@Data//lombok註解class Person implements Cloneable{ private int age; private String name; private Address address; public Person(int age, String name, Address address) { this.age = age; this.name = name; this.address = address; } @Override protected Object clone() throws CloneNotSupportedException { return super.clone(); } public String display() { return "Person [age=" + age + ", name=" + name + ", address=" + address + "]"; }}@Data//lombok註解class Address{ private String province; private String street; public Address(String province, String street) { this.province = province; this.street = street; } @Override public String toString() { return "Address [province=" + province + ", street=" + street + "]"; }}public class Client{ public static void main(String[] args) throws CloneNotSupportedException { Person person = new Person(15, "sowhat", new Address("河北", "建華南大街")); Person clonePerson = (Person) person.clone(); System.out.println(person); System.out.println(clonePerson); // 信息完全一樣 System.out.println(person.display()); System.out.println(clonePerson.display()); System.out.println("信息完全一致"); System.out.println("原始年齡:" + person.getAge()); System.out.println("克隆後原始年齡:" + clonePerson.getAge()); System.out.println("年齡完全一樣"); System.out.println("原始名字哈希值:" + person.getName().hashCode()); System.out.println("克隆後名字哈希值:" + clonePerson.getName().hashCode()); System.out.println("字符串哈希值完全一樣"); clonePerson.setName("xiaomai"); clonePerson.setAge(20); clonePerson.getAddress().setStreet("中山路"); System.out.println(clonePerson.display()); System.out.println(person.display()); System.out.println("年齡跟姓名 是完全的深拷貝 副本跟原值無關的!"); System.out.println("地址信息的修改是淺拷貝 "); }}
結果如下:
mytest.Person@15f550a
mytest.Person@6b2d4a
Person [age=15, name=sowhat, address=Address [province=河北, street=建華南大街]]
Person [age=15, name=sowhat, address=Address [province=河北, street=建華南大街]]
信息完全一致
原始年齡:15
克隆後原始年齡:15
年齡完全一樣
原始名字哈希值:-1432601412
克隆後名字哈希值:-1432601412
字符串哈希值完全一樣
Person [age=20, name=xiaomai, address=Address [province=河北, street=中山路]]
Person [age=15, name=sowhat, address=Address [province=河北, street=中山路]]
結論:
原對象與新對象是兩個不同的對象。拷貝出來的新對象與原對象內容一致接著將新對象裡面的信息進行了修改,然後輸出發現原對象裡面的部分信息也跟著變了。其中基本類型跟 String類型的改變不會影響到原始對象的改變。而其他的Ojbect 類型改變的時候會影響到原始數據。上面的結論稱為淺拷貝。即創建一個新對象,然後將當前對象的非靜態欄位複製到該對象,如果欄位類型是值類型(基本類型跟String)的,那麼對該欄位進行複製;如果欄位是引用類型的,則只複製該欄位的引用而不複製引用指向的對象(也就是只複製對象的地址)。此時新對象裡面的引用類型欄位相當於是原始對象裡面引用類型欄位的一個副本,原始對象與新對象裡面的引用欄位指向的是同一個對象。因此,修改clonePerson裡面的address內容時,原person裡面的address內容會跟著改變。
深拷貝
了解了淺拷貝,那麼深拷貝是什麼也就很清楚了。那麼該如何實現深拷貝呢?Object 類提供的 clone 是只能實現淺拷貝的,即將引用類型的屬性內容也拷貝一份新的。
那麼,實現深拷貝我這裡收集到兩種方式:第一種是給需要拷貝的引用類型也實現Cloneable接口並覆寫clone方法;第二種則是利用序列化。
接下來分別對兩種方式進行演示:
深拷貝-clone方式
對於以上演示代碼,利用clone方式進行深拷貝無非就是將Address類也實現Cloneable,然後對Person的clone方法進行調整。讓每個引用類型屬性內部都重寫clone() 方法,既然引用類型不能實現深拷貝,那麼我們將每個引用類型都拆分為基本類型,分別進行淺拷貝。比如上面的例子,Person 類有一個引用類型 Address(其實String 也是引用類型,但是String類型有點特殊,後面會詳細講解),我們在 Address 類內部也重寫 clone 方法。如下:
package mytest;@Data//lombok註解class Person implements Cloneable{ private int age; private String name; private Address address; protected int abc = 12; public Person(int age, String name, Address address) { this.age = age; this.name = name; this.address = address; } @Override // clone 重載 protected Object clone() throws CloneNotSupportedException { Person person = (Person) super.clone(); //手動對address屬性進行clone,並賦值給新的person對象 person.address = (Address) address.clone(); return person; } public String display() { return "Person [age=" + age + ", name=" + name + ", address=" + address + "]"; }}@Data//lombok註解class Address implements Cloneable{ private String province; private String street; public Address(String province, String street) { this.province = province; this.street = street; } // 深拷貝時添加 @Override protected Object clone() throws CloneNotSupportedException { return super.clone(); } @Override public String toString() { return "Address [province=" + province + ", street=" + street + "]"; }}public class Client{ public static void main(String[] args) throws CloneNotSupportedException { Person person = new Person(15, "sowhat", new Address("河北", "建華南大街")); Person p1 = person; p1.setAge(45); System.out.println(p1.hashCode()); System.out.println(person.hashCode()); System.out.println(p1.display()); System.out.println(person.display()); System.out.println("-----------"); Person clonePerson = (Person) person.clone(); System.out.println(person); System.out.println(clonePerson); // 信息完全一樣 System.out.println(person.display()); System.out.println(clonePerson.display()); System.out.println("信息完全一致"); System.out.println("原始年齡:" + person.getAge()); System.out.println("克隆後原始年齡:" + clonePerson.getAge()); System.out.println("年齡完全一樣"); System.out.println("原始名字哈希值:" + person.getName().hashCode()); System.out.println("克隆後名字哈希值:" + clonePerson.getName().hashCode()); System.out.println("字符串哈希值完全一樣"); clonePerson.setName("sowhat1412"); clonePerson.setAge(20); clonePerson.getAddress().setStreet("中山路"); System.out.println(clonePerson.display()); System.out.println(person.display()); System.out.println("年齡跟姓名 是完全的深拷貝 副本跟原值無關的!"); System.out.println("地址信息的修改是淺拷貝 "); }}
但是這種做法有個弊端,這裡我們Person 類只有一個 Address 引用類型,而 Address 類沒有,所以我們只用重寫 Address 類的clone 方法。但是如果 Address 類也存在一個引用類型,那麼我們也要重寫其clone 方法,這樣下去,有多少個引用類型,我們就要重寫多少次,如果存在很多引用類型,那麼代碼量顯然會很大,所以這種方法不太合適。
利用序列化
序列化是將對象寫到流中便於傳輸,而反序列化則是把對象從流中讀取出來。這裡寫到流中的對象則是原始對象的一個拷貝,因為原始對象還存在 JVM 中,所以我們可以利用對象的序列化產生克隆對象,然後通過反序列化獲取這個對象。注意每個需要序列化的類都要實現 Serializable 接口,如果有某個屬性不需要序列化,可以將其聲明為 transient,即將其排除在克隆屬性之外。
package mytest;import java.io.ByteArrayInputStream;import java.io.ByteArrayOutputStream;import java.io.ObjectInputStream;import java.io.ObjectOutputStream;import java.io.Serializable;/** * 利用序列化和反序列化進行對象的深拷貝 * @author ljj */classDeepCloneimplementsSerializable{privatestaticfinallong serialVersionUID = 1412L;public Object deepClone()throws Exception{//序列化 ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(bos); oos.writeObject(this);//反序列化 ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray()); ObjectInputStream ois = new ObjectInputStream(bis);return ois.readObject(); }}@DataclassPersonextendsDeepClone{privatestaticfinallong serialVersionUID = 1L;privateint age;private String name;private Address address;publicPerson(int age, String name, Address address){this.age = age;this.name = name;this.address = address; }public String display(){return"Person [age=" + age + ", name=" + name + ", address=" + address + "]"; }}@DataclassAddressextendsDeepClone{privatestaticfinallong serialVersionUID = 1412L;private String province;private String street;publicAddress(String province, String street){this.province = province;this.street = street; }@Overridepublic String toString(){return"Address [province=" + province + ", street=" + street + "]"; }publicvoidsetStreet(String street){this.street = street; }}publicclassClient{publicstaticvoidmain(String[] args)throws Exception{ Person person = new Person(15, "sowhat", new Address("河北", "建華南大街")); Person clonePerson = (Person) person.deepClone(); System.out.println(person); System.out.println(clonePerson); System.out.println(person.display()); System.out.println(clonePerson.display()); clonePerson.setName("sowhat1412"); clonePerson.setAge(20); Address address = clonePerson.getAddress(); address.setStreet("中山路"); System.out.println(clonePerson.display()); System.out.println(person.display()); }}