前言
Java 開發中,物件複製是常有的事,很多人可能搞不清到底是複製了引用還是複製了物件。本文將詳細介紹相關知識,讓你充分理解 Java 複製。
一、物件是如何儲存的?
方法執行過程中,方法體中的資料型別主要分兩種,它們的儲存方式是不同的(如下圖):
基本資料型別: 直接儲存在棧幀的區域性變數表中;
引用資料型別: 物件的引用儲存在棧幀的區域性變數表中,而對例項本身及其所有成員變數存放在堆記憶體中。
二、前置準備
建立兩個實體類方便後續的程式碼示例
@Data @AllArgsConstructor public class Animal{ private int id; private String type; @Override public String toString () { return "Animal{" + "id=" + id + ", type='" + type + '\'' + '}'; } }
@Data @AllArgsConstructor public class Dog { private int age; private String name; private Animal animal; @Override public String toString () { return "Dog{" + "age=" + age + ", name='" + name + '\'' + ", animal=" + animal + '}'; } }
三、直接賦值
直接賦值是我們最常用的方式,它只是複製了物件引用地址,並沒有在記憶體中生成新的物件。
下面我們進行程式碼驗證:
public class FuXing { public static void main (String[] args) { Animal animal = new Animal(1, "dog"); Dog dog = new Dog(18, "husky", animal); Dog dog2 = dog; System.out.println("兩個物件是否相等:" + (dog2 == dog)); System.out.println("----------------------------"); dog.setAge(3); System.out.println("變化後兩個物件是否相等:" + (dog2 == dog)); } }
兩個物件是否相等:true ---------------------------- 變化後兩個物件是否相等:true
透過執行結果可知,dog類的age已經發生變化,但重新列印兩個類依然相等。所以它只是複製了物件引用地址,並沒有在記憶體中生成新的物件。
直接賦值的 JVM 的記憶體結構大致如下:
四、淺複製
淺複製後會建立一個新的物件,且新物件的屬性和原物件相同。但是,複製時針對原物件的屬性的資料型別的不同,有兩種不同的情況:
屬性的資料型別基本型別,複製的就是基本型別的值;
屬性的資料型別引用型別,複製的就是物件的引用地址,意思就是複製物件與原物件引用同一個物件。
要實現物件淺複製還是比較簡單的,只需要被複製的類實現Cloneable介面,重寫clone方法即可。下面我們對Dog進行改動:
@Data @AllArgsConstructor public class Dog implements Cloneable{ private int age; private String name; private Animal animal; @Override public Dog clone () throws CloneNotSupportedException { return (Dog) super.clone(); } @Override public String toString () { return "Dog{" + "age=" + age + ", name='" + name + '\'' + ", animal=" + animal + '}'; } }
接下來我們執行下面的程式碼,看一下執行結果:
public class FuXing { public static void main (String[] args) throws Exception { Animal animal = new Animal(1, "dog"); Dog dog = new Dog(18, "husky", animal); // 克隆物件 Dog cloneDog = dog.clone(); System.out.println("dog:" + dog); System.out.println("cloneDog:" + cloneDog); System.out.println("兩個物件是否相等:" + (cloneDog == dog)); System.out.println("兩個name是否相等:" + (cloneDog.getName() == dog.getName())); System.out.println("兩個animal是否相等:" + (cloneDog.getAnimal() == dog.getAnimal())); System.out.println("----------------------------------------"); // 更改原物件的屬性值 dog.setAge(3); dog.setName("corgi"); dog.getAnimal().setId(2); System.out.println("dog:" + dog); System.out.println("cloneDog:" + cloneDog); System.out.println("兩個物件是否相等:" + (cloneDog == dog)); System.out.println("兩個name是否相等:" + (cloneDog.getName() == dog.getName())); System.out.println("兩個animal是否相等:" + (cloneDog.getAnimal() == dog.getAnimal())); }
dog:Dog{age=18, name='husky', animal=Animal{id=1, type='dog'}} cloneDog:Dog{age=18, name='husky', animal=Animal{id=1, type='dog'}} 兩個物件是否相等:false 兩個name是否相等:true 兩個animal是否相等:true ---------------------------------------- dog:Dog{age=3, name='corgi', animal=Animal{id=2, type='dog'}} cloneDog:Dog{age=18, name='husky', animal=Animal{id=2, type='dog'}} 兩個物件是否相等:false 兩個name是否相等:false 兩個animal是否相等:true
我們分析下執行結果,重點看一下 “兩個name是否相等”,改動後變成 false.
這是因為String、Integer等包裝類都是不可變的物件,當需要修改不可變物件的值時,需要在記憶體中生成一個新的物件來存放新的值,然後將原來的引用指向新的地址。
這裏dog物件的name屬性已經指向一個新的物件,而cloneDog的name屬性仍然指向原來的物件,所以就不同了。
然後我們看下兩個物件的animal屬性,原物件屬性值變動後,複製物件也跟著變動,這就是因為複製物件與原物件引用同一個物件。
淺複製的 JVM 的記憶體結構大致如下:
五、深複製
與淺複製不同之處,深複製在對引用資料型別進行複製的時候,建立了一個新的物件,並且複製其成員變數。也就是說,深複製出來的物件,與原物件沒有任何關聯,是一個新的物件。
實現深複製有兩種方式
1. 讓每個引用型別屬性都重寫clone()方法
注意: 這裏如果引用型別的屬性或者層數太多了,程式碼量會變很大,所以一般不建議使用
@Data @AllArgsConstructor public class Animal implements Cloneable{ private int id; private String type; @Override protected Animal clone () throws CloneNotSupportedException { return (Animal) super.clone(); } @Override public String toString () { return "Animal{" + "id=" + id + ", type='" + type + '\'' + '}'; } }
@Data @AllArgsConstructor public class Dog implements Cloneable{ private int age; private String name; private Animal animal; @Override public Dog clone () throws CloneNotSupportedException { Dog clone = (Dog) super.clone(); clone.animal = animal.clone(); return clone; } @Override public String toString () { return "Dog{" + "age=" + age + ", name='" + name + '\'' + ", animal=" + animal + '}'; } }
我們再次執行淺複製部分的main方法,結果如下。
dog:Dog{age=18, name='husky', animal=Animal{id=1, type='dog'}} cloneDog:Dog{age=18, name='husky', animal=Animal{id=1, type='dog'}} 兩個物件是否相等:false 兩個name是否相等:true 兩個animal是否相等:false # 變為false ---------------------------------------- dog:Dog{age=3, name='corgi', animal=Animal{id=2, type='dog'}} cloneDog:Dog{age=18, name='husky', animal=Animal{id=1, type='dog'}} 兩個物件是否相等:false 兩個name是否相等:false 兩個animal是否相等:false # 變為false
2.序列化
序列化是將物件寫到流中便於傳輸,而反序列化則是把物件從流中讀取出來。我們可以利用物件的序列化產生克隆物件,然後透過反序列化獲取這個物件。
@Data @AllArgsConstructor public class Animal implements Serializable { private int id; private String type; @Override public String toString () { return "Animal{" + "id=" + id + ", type='" + type + '\'' + '}'; } }
@Data @AllArgsConstructor public class Dog implements Serializable { private int age; private String name; private Animal animal; @SneakyThrows @Override public Dog clone () { // 序列化 ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(bos); oos.writeObject(this); //反序列化 ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray()); ObjectInputStream ois = new ObjectInputStream(bis); return (Dog) ois.readObject(); } @Override public String toString () { return "Dog{" + "age=" + age + ", name='" + name + '\'' + ", animal=" + animal + '}'; } }
我們再次執行淺複製部分的main方法,結果如下。
dog:Dog{age=18, name='husky', animal=Animal{id=1, type='dog'}} cloneDog:Dog{age=18, name='husky', animal=Animal{id=1, type='dog'}} 兩個物件是否相等:false 兩個name是否相等:false # 變為false 兩個animal是否相等:false # 變為false ---------------------------------------- dog:Dog{age=3, name='corgi', animal=Animal{id=2, type='dog'}} cloneDog:Dog{age=18, name='husky', animal=Animal{id=1, type='dog'}} 兩個物件是否相等:false 兩個name是否相等:false 兩個animal是否相等:false # 變為false
深複製的 JVM 的記憶體結構大致如下: