天天看點

Java基礎系列-淺拷貝和深拷貝

原創文章,轉載請标注出處:https://www.cnblogs.com/V1haoge/p/10755377.html

一、概述

Java中的拷貝功能是由Object類的clone方法定義的。

public class Object{
    //...
    protected native Object clone() throws CloneNotSupportedException;
    //...
}
           

這是一個可被重寫的本地方法。

這個方法是與Cloneable接口相關聯的,如果針對一個沒有實作Cloneable接口的方法執行器clone方法,會抛出CloneNotSupportedException異常。

二、Cloneable

Cloneable接口就是一個标記接口,表示實作了該接口的類具備了可以克隆的功能。

一般來說,如果一個類實作了該接口,那麼同時需要重寫Object中的clone方法。

這裡要謹記一點:clone方法是Object中定義的方法,而不是Cloneable中定義的,後者隻是一個标記接口,其中沒有任何方法定義。

三、clone()

當一個類實作了Cloneable之後,具備了可被克隆的基礎條件,下面就需要具體完善克隆工作的細節了。

細節當然需要依靠重寫clone方法來實作。

到這裡就涉及到了本文的重點:淺拷貝與深拷貝。

簡單說下二者的差別:

淺拷貝就是克隆出來的新對象與原對象完全一緻無二,包括引用類型字段的值。這就會有一個現象,針對引用類型的字段,兩個對象的引用位址一緻,如此一來新舊對象之間強關聯,修改其中一個對象的内容,極可能影響到另外一個的内容。

深拷貝是針對淺拷貝而言的,那就是将所有引用的對象同樣做深拷貝,這樣一來,新舊對象就不再強關聯,當我們修改其中一個對象時,不會影響到另外一個。

3.1 淺拷貝

實作淺拷貝非常簡單,直接調用Object中定義的clone方法即可。也即,Object中定義的clone方法實作的是淺拷貝功能。

讓我們來看看下面的例子:

class A {
    int i = 20;
    A(){}
    A(int i){
        this.i = i;
    }
}
class B implements Cloneable{
    private int i = 10;
    private A a = new A(11);
    public Object clone () {
        B b = null;
        try {
            b = (B)super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return b;
    }
    public A getA() {
        return a;
    }
    public void setA(A a) {
        this.a = a;
    }
}
public class CloneTest {
    public static void main(String[] args) {
        B b1 = new B();
       // 執行淺拷貝
       System.out.println("執行淺拷貝:");
       B cb1 = (B)b.clone();
       System.out.println(cb1.equals(b));// false-表示clone的對象與原對象不同
       System.out.println((cb1.getA()).equals(b.getA()));// true-表示淺拷貝引用對象不變
    }
}
           

執行結果為:

執行淺拷貝:
false
true
           

通過執行個體我們可以看出,要寫實作淺拷貝需要如下幾步:

  • 被拷貝對象的類實作Cloneable接口
  • 被拷貝對象的類重寫Object的clone方法
  • 執行拷貝操作,就是調用重寫的clone方法來完成克隆

從執行個體的執行結果中可以看出新的clone對象執行個體cb1與原對象執行個體b1,之間equals不等,這裡的equals比較的是兩個對象的位址,與“==”效果一緻。而cb1的a字段指向的A執行個體與b1的a字段指向的A執行個體卻是一樣的,這就是淺拷貝。

淺拷貝是淺層拷貝,隻在意目标對象一個,與其關聯的其他對象一概不管。

但是在實際使用中,使用淺拷貝的情況很少,多數情況下,我們需要的是一個完全嶄新的對象,包括它的字段引用,那麼我們就需要實作深拷貝了。

3.2 深拷貝

所謂深拷貝就是相對淺拷貝而言的,在淺拷貝的基礎上進一步進行深層拷貝即可,那麼具體實作呢?

class A implements Cloneable{
    int i = 20;
    A(){}
    A(int i){
        this.i = i;
    }
    public Object clone (){
        A a = null;
        try {
            a = (A)super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return a;
    }
}
class B implements Cloneable{
    private int i = 10;
    private A a = new A(11);
    public Object clone () {
        B b = null;
        try {
            b = (B)super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return b;
    }
    public A getA() {
        return a;
    }
    public void setA(A a) {
        this.a = a;
    }
}
public class CloneTest {
    public static void main(String[] args) {
        B b = new B();
        // 執行淺拷貝
        System.out.println("執行淺拷貝:");
        B cb1 = (B)b.clone();
        System.out.println(cb1.equals(b));// false-表示clone的對象與原對象不同
        System.out.println((cb1.getA()).equals(b.getA()));// true-表示淺拷貝引用對象不變
        // 執行深拷貝
        System.out.println("----------------");
        System.out.println("執行深拷貝:");
        B cb2 = (B)b.clone();
        A ca = (A)b.getA().clone();
        cb2.setA(ca);
        System.out.println(cb2.equals(b));// false-表示clone的對象與原對象不同
        System.out.println((cb2.getA()).equals(b.getA()));// true-表示淺拷貝引用對象不變
    }
}
           

執行結果:

執行淺拷貝:
false
true
----------------
執行深拷貝:
false
false
           

通過執行個體我們可以清楚看出,要想實作深拷貝需要如下幾步:

  • 被拷貝的目标對象的類的依賴對象的類A需要實作Cloneable接口
  • 類A需要重寫Object的clone方法
  • 在b拷貝完成後,要繼續執行b中被依賴對象a的拷貝,并将拷貝結果指派給b的拷貝對象。

可見深拷貝其實就是一個遞歸拷貝的過程,将目标對象的類依賴的所有對象的類全部實作Cloneable接口并重寫clone方法進行二層拷貝,然後是依賴對象的依賴對象...層層深入拷貝。

四、注意事項

Java中提供的類中存在一些無法被clone的類,比如StringBuffer,它既沒有實作Cloneable接口,也沒有重寫clone方法,那麼它就無法執行克隆操作,那麼遇到它該怎麼辦呢?

這裡提供一種辦法,既然是新對象,那麼直接建立一個新的StringBuffer對象,将原來的值指派進來,就等于是完成了克隆,雖然這種方式要比Object中的clone方法慢許多,但不失為一種補救措施。

還有一些類型如基本類型的裝箱類型、String,它們雖然也是引用類型,但是它們是final修飾的,且沒有實作Cloneable接口,沒有重寫clone方法,要克隆也需要采用上面所示的方式來實作。

參考:

  • java Clone使用方法詳解