前言
Java 開發中,物件複製是常有的事,很多人可能搞不清到底是複製了引用還是複製了物件。本文將詳細介紹相關知識,讓你充分理解 Java 複製。
一、物件是如何儲存的?
方法執行過程中,方法體中的資料型別主要分兩種,它們的儲存方式是不同的(如下圖):
- 基本資料型別: 直接儲存在棧幀的區域性變數表中;
- 引用資料型別: 物件的引用儲存在棧幀的區域性變數表中,而對例項本身及其所有成員變數存放在堆記憶體中。
詳情可見JVM基礎
二、前置準備
建立兩個實體類方便後續的程式碼示例
@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 的記憶體結構大致如下: