Java 淺拷貝和深拷貝

淺拷貝

  • 對於數據類型是基本數據類型的字段,淺拷貝會直接進行值傳遞,也就是將該屬性值複製一份給新的對象。因爲是兩份不同的數據,所以對其中一個對象的該字段值進行修改,不會影響另一個對象拷貝得到的數據。也就是說基本數據類型可以自動實現深拷貝。
  • 對於數據類型是引用數據類型的字段,比如說字段是某個數組、某個類的對象等,那麼淺拷貝會進行引用傳遞,也就是隻是將該字段的引用值(內存地址)複製一份給新的對象。因爲實際上兩個對象的該字段都指向同一個實例。在這種情況下,在一個對象中修改該字段會影響到另一個對象的該字段值。
  • 對於StringInteger 等不可變類,都應用了常量池技術。只要每次使用 setter 方法修改了 String 類型的字段值,都會在常量池中新生成一個新的字符串常量並返回一個新的引用值給字段,每次都不相同,所以不可變類在拷貝過程中的效果其實是等同於基本數據類型的。假設如果想在修改引用類型字段時達到修改其引用值的效果,那麼需要:a.setB(new B())
    在這裏插入圖片描述

由上圖可以看到基本數據類型的字段,對其值創建了新的拷貝。而引用數據類型的字段的實例仍然是隻有一份,兩個對象的該字段都指向同一個實例。

類和字段

新建一個類 ShallowSchool,其中有 String 類型的字段 namelevel。注意,在實現淺拷貝時,此時作爲後面 ShallowStudent 類的引用類型字段的 ShallowSchool 無需實現 Clonable 接口。

public class ShallowSchool {

    private String name;

    private String level;

    public ShallowSchool(String name, String level) {
        this.name = name;
        this.level = level;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getLevel() {
        return level;
    }

    public void setLevel(String level) {
        this.level = level;
    }
    
}

新建另一個類 ShallowStudent ,其中有 Integer類型的字段 idString 類型的字段 name 和引用類型字段 ShallowSchool。這個類要實現 Clonable 接口,並重寫 clone 方法。

public class ShallowStudent implements Cloneable {

    private Integer id;

    private String name;

    private ShallowSchool shallowSchool;

    public ShallowStudent(Integer id, String name, ShallowSchool shallowSchool) {
        this.id = id;
        this.name = name;
        this.shallowSchool = shallowSchool;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public ShallowSchool getShallowSchool() {
        return shallowSchool;
    }

    public void setShallowSchool(ShallowSchool shallowSchool) {
        this.shallowSchool = shallowSchool;
    }

    @Override
    protected ShallowStudent clone() throws CloneNotSupportedException {
        return (ShallowStudent) super.clone();
    }

}

其重寫的 clone 方法中調用了默認父類 Objectclone 方法

protected native Object clone() throws CloneNotSupportedException;

但斷點進入後發現 super 對象是 ShallowStudent 對象本身,與 this 爲同一對象
在這裏插入圖片描述

單元測試

對淺克隆做全面的單元測試,以瞭解其特性。

@Test
public void shallowCopy() throws CloneNotSupportedException {
    ShallowSchool school = new ShallowSchool("CUG", "211");
    ShallowStudent student = new ShallowStudent(1, "Jake Weng", school);
    ShallowStudent shallowClonedStudent = student.clone();
    assertSame(student.getName(), shallowClonedStudent.getName());
    assertSame(student.getShallowSchool(), shallowClonedStudent.getShallowSchool());
    assertNotSame(student, shallowClonedStudent);

    school.setName("WHUT");
    assertSame(student.getShallowSchool(), shallowClonedStudent.getShallowSchool());

    ShallowSchool anotherSchool = new ShallowSchool("WHUT", "211");
    student.setShallowSchool(anotherSchool);
    assertNotSame(student.getShallowSchool(), shallowClonedStudent.getShallowSchool());

    student.setName("Weng ZhengKai");
    assertNotEquals(student.getName(), shallowClonedStudent.getName());
    assertSame("Jake Weng", shallowClonedStudent.getName());
    assertSame("Weng ZhengKai", student.getName());
    String englishName = new String("Jake Weng");
    String chineseName = new String("Weng ZhengKai");
    assertNotSame(englishName, shallowClonedStudent.getName());
    assertNotSame(chineseName, student.getName());
    assertEquals(englishName, shallowClonedStudent.getName());
    assertEquals(chineseName, student.getName());

    student.setId(2);
    assertNotEquals(student.getId(), shallowClonedStudent.getId());
    assertSame(1, shallowClonedStudent.getId());
    assertSame(2, student.getId());
    Integer oldStudentId = new Integer(1);
    Integer newStudentId = new Integer(2);
    assertNotSame(oldStudentId, shallowClonedStudent.getId());
    assertNotSame(newStudentId, student.getId());
    assertEquals(oldStudentId, shallowClonedStudent.getId());
    assertEquals(newStudentId, student.getId());
}

以上 shallowCopy() 方法中的所有斷言均能夠通過。
以上單元測試可以驗證以下結論:

  • 淺拷貝得到的引用類型字段與原字段確實是共享同一引用,若兩者其中之一又對自己內部的字段做了修改,那麼另一引用類型字段可以感知到這種修改。
  • 克隆得到的對象與原對象不是同一引用,這一點與等號直接賦值不同。
  • 改變原對象(克隆對象)的不可變類型字段的值並不會影響克隆對象(原對象)的不可變類型字段。
  • 改變不可變字段的值,如果改變的值不存在,那麼是新建了一個常量放入池中,並將該常量的引用返回給不可變字段;如果改變的值已存在,那麼是將該常量的引用直接返回給不可變字段。
  • 由於 JVM 常量池的存在,所以 String 類型的字段對比能夠通過 assertSame的斷言測試。
  • 如果使用構造方法(new)來創建一個不可變對象,即使其值在常量池中已存在,仍相當於在常量池中新建了一個常量並返回其引用。
  • 如果想在修改引用類型字段時達到和修改不可變類型字段一樣的效果,那麼需要 a.setB(new B())

深拷貝(重寫 clone 方法)

首先介紹對象圖的概念。設想一下,一個類有一個對象,其字段中又有一個對象,該對象指向另一個對象,另一個對象又指向另一個對象,直到一個確定的實例。這就形成了對象圖。那麼,對於深拷貝來說,不僅要複製對象的所有基本數據類型的字段值,還要爲所有引用數據類型的字段申請存儲空間,並複製每個引用數據類型字段所引用的對象,直到該對象可達的所有對象。也就是說,對象進行深拷貝要對整個對象圖進行拷貝。
簡單地說,深拷貝對引用數據類型的字段的對象圖中所有的對象都開闢了內存空間;而淺拷貝只是傳遞地址指向,新的對象並沒有對引用數據類型創建內存空間。
深拷貝模型如圖所示,可以看到所有的字段都進行了複製。
在這裏插入圖片描述

類和字段

同之前淺拷貝的代碼,新建 DeepSchoolDeepStudent 兩個類及其相應字段。

public class DeepSchool implements Cloneable {

    private String name;

    private String level;

    public DeepSchool(String name, String level) {
        this.name = name;
        this.level = level;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getLevel() {
        return level;
    }

    public void setLevel(String level) {
        this.level = level;
    }

    @Override
    public DeepSchool clone() throws CloneNotSupportedException {
        return (DeepSchool) super.clone();
    }

}
public class DeepStudent implements Cloneable {

    private Integer id;

    private String name;

    private DeepSchool deepSchool;

    public DeepStudent(Integer id, String name, DeepSchool deepSchool) {
        this.id = id;
        this.name = name;
        this.deepSchool = deepSchool;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public DeepSchool getDeepSchool() {
        return deepSchool;
    }

    public void setDeepSchool(DeepSchool deepSchool) {
        this.deepSchool = deepSchool;
    }

    @Override
    protected DeepStudent clone() throws CloneNotSupportedException {
        DeepStudent deepStudent = (DeepStudent) super.clone();
        deepStudent.deepSchool = deepSchool.clone();
        return deepStudent;
    }

}

在深拷貝中,DeepSchoolDeepStudent 都實現了 Cloneable 接口。DeepSchool 重寫的 clone 方法中僅僅調用了 super.clone();而 DeepStudentclone 方法不僅調用了 super.clone(),還將字段 deepSchool 的克隆結果賦值給克隆出的 deepStudentdeepSchool 字段。由此可以看出,不僅被克隆的類要實現 Cloneable 接口,而且其引用類型字段的類也實現 Cloneable 接口,並且在被克隆的類重寫的 clone() 方法中將引用類型的字段一一克隆、並賦值給自己的克隆對象,才能做到深拷貝。如果一個類中的引用類型字段過多,那麼採用這種方式進行深拷貝會非常麻煩,因爲每個引用類都要實現 Cloneable 接口並重寫 clone() 方法,而且自身的 clone() 方法代碼會非常冗長。

單元測試

@Test
public void deepCopy() throws CloneNotSupportedException {
	DeepSchool school = new DeepSchool("CUG", "211");
	DeepStudent student = new DeepStudent(2, "Jake Weng", school);
	DeepStudent deepClonedStudent = student.clone();
	assertSame(student.getId(), deepClonedStudent.getId());
	assertSame(student.getName(), deepClonedStudent.getName());
	assertSame(student.getDeepSchool().getName(), deepClonedStudent.getDeepSchool().getName());
	assertSame(student.getDeepSchool().getLevel(), deepClonedStudent.getDeepSchool().getLevel());
	assertNotSame(student.getDeepSchool(), deepClonedStudent.getDeepSchool());
	assertNotSame(student, deepClonedStudent);

	school.setName("WHUT");
	assertNotSame(student.getDeepSchool(), deepClonedStudent.getDeepSchool());
	assertSame("WHUT", student.getDeepSchool().getName());
	assertSame("CUG", deepClonedStudent.getDeepSchool().getName());

	DeepStudent anotherStudent = new DeepStudent(2, "Jake Weng", new DeepSchool("WHUT", "211"));
	assertSame(student.getId(), anotherStudent.getId());
	assertSame(student.getName(), anotherStudent.getName());
	assertSame(student.getDeepSchool().getName(), anotherStudent.getDeepSchool().getName());
	assertSame(student.getDeepSchool().getLevel(), anotherStudent.getDeepSchool().getLevel());
	assertNotSame(student.getDeepSchool(), anotherStudent.getDeepSchool());
	assertNotSame(student, anotherStudent);
}

由單元測試得出結論:

  • 由於 JVM 常量池的存在,所以對比不可變類字段的 assertSame 斷言可以通過。
  • 深拷貝之後,原對象與克隆對象中的引用類型字段不再共享同一引用。

深拷貝(序列化)

與上述實現 Cloneable 並在對象及其所有引用類型字段中重寫 clone() 方法不同,這種方法只需實現 Serializable 接口即可,隨後使用 Java 中的 I/O 流進行深拷貝。

類和字段

public class SerializableSchool implements Serializable {

    private String name;

    private String level;

    public SerializableSchool(String name, String level) {
        this.name = name;
        this.level = level;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getLevel() {
        return level;
    }

    public void setLevel(String level) {
        this.level = level;
    }
}
public class SerializableStudent implements Serializable {

    private Integer id;

    private String name;

    private SerializableSchool school;

    public SerializableStudent(Integer id, String name, SerializableSchool school) {
        this.id = id;
        this.name = name;
        this.school = school;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public SerializableSchool getSchool() {
        return school;
    }

    public void setSchool(SerializableSchool school) {
        this.school = school;
    }
}

單元測試

@Test
public void serializableDeepCopyWithObjectOutputStream() {
	SerializableSchool school = new SerializableSchool("CUG", "211");
	SerializableStudent student = new SerializableStudent(1, "Jake Weng", school);
	try {
		ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("student.txt"));
		objectOutputStream.writeObject(student);
		objectOutputStream.close();
		ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("student.txt"));
		SerializableStudent readStudent = (SerializableStudent) objectInputStream.readObject();

		assertSame(1, student.getId());
		assertNotSame(1, readStudent.getId());
		assertSame("Jake Weng", student.getName());
		assertNotSame("Jake Weng", readStudent.getName());

		// 反序列化後的字段與原字段的值相等,但卻不是同一引用。
		assertEquals(student.getId(), readStudent.getId());
		assertEquals(student.getName(), readStudent.getName());
		assertEquals(student.getSchool().getName(), readStudent.getSchool().getName());
		assertEquals(student.getSchool().getLevel(), readStudent.getSchool().getLevel());
		assertNotSame(student.getId(), readStudent.getId());
		assertNotSame(student.getName(), readStudent.getName());
		assertNotSame(student.getSchool().getName(), readStudent.getSchool().getName());
		assertNotSame(student.getSchool().getLevel(), readStudent.getSchool().getLevel());
		assertNotSame(student.getSchool(), readStudent.getSchool());
		assertNotSame(student, readStudent);

		student.setName("Weng ZhengKai");
		school.setName("WHUT");
		assertNotEquals(student.getName(), readStudent.getName());
		assertNotEquals(student.getSchool().getName(), readStudent.getSchool().getName());

		readStudent.setName("Weng ZhengKai");
        assertSame(student.getName(), readStudent.getName());
	} catch (Exception e) {
		e.printStackTrace();
	}
}
@Test
public void serializableDeepCopyWithByteArrayOutputStream() {
	SerializableSchool school = new SerializableSchool("CUG", "211");
	SerializableStudent student = new SerializableStudent(1, "Jake Weng", school);
	try {
		ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
		ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
		objectOutputStream.writeObject(student);
		objectOutputStream.close();
		ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
		ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
		SerializableStudent readStudent = (SerializableStudent) objectInputStream.readObject();

		assertSame(1, student.getId());
		assertNotSame(1, readStudent.getId());
		assertSame("Jake Weng", student.getName());
		assertNotSame("Jake Weng", readStudent.getName());

		// 反序列化後的字段與原字段的值相等,但卻不是同一引用。
		assertEquals(student.getId(), readStudent.getId());
		assertEquals(student.getName(), readStudent.getName());
		assertEquals(student.getSchool().getName(), readStudent.getSchool().getName());
		assertEquals(student.getSchool().getLevel(), readStudent.getSchool().getLevel());
		assertNotSame(student.getId(), readStudent.getId());
		assertNotSame(student.getName(), readStudent.getName());
		assertNotSame(student.getSchool().getName(), readStudent.getSchool().getName());
		assertNotSame(student.getSchool().getLevel(), readStudent.getSchool().getLevel());
		assertNotSame(student.getSchool(), readStudent.getSchool());
		assertNotSame(student, readStudent);

		student.setName("Weng ZhengKai");
		school.setName("WHUT");
		assertNotEquals(student.getName(), readStudent.getName());
		assertNotEquals(student.getSchool().getName(), readStudent.getSchool().getName());

		readStudent.setName("Weng ZhengKai");
        assertSame(student.getName(), readStudent.getName());
	} catch (Exception e) {
		e.printStackTrace();
	}
}

由上述單元測試可知使用 I/O 流進行對象的序列化和反序列化有兩種方式:

  1. ObjectOutputStream & ObjectInputStream
  2. ByteArrayOutputStream & ByteArrayInputStream

而由單元測試的結果可以得出以下結論:

  • 進行反序列化後得到的克隆對象中不僅引用類型字段與原對象完全獨立,互不影響;甚至連不可變字段的常量池也不再與原對象共享,即使用序列化深拷貝後的不可變字段只是值相等,而引用不相等。
  • 使用 setter 方法改變序列化深拷貝的對象的不可變字段值後,與原對象又開始共享常量池,所以最後的 assertSame 能夠通過。

參考博客

發佈了85 篇原創文章 · 獲贊 335 · 訪問量 10萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章