天天看點

沒看這篇文章之前,我以為真的懂深克隆和淺克隆。

面試題:深克隆和淺克隆的實作方式

面試官考察點

考察目的: 深克隆和淺克隆,考察的是Java基礎知識的了解。

考察人群: 2到5年開發經驗。

背景知識詳解

先了解下淺克隆和深克隆的定義:

  1. 淺克隆:被複制對象的所有變量都含有與原來的對象相同的值,而所有的對其他對象的引用仍然指向原來的對象。
  2. 深克隆:除去那些引用其他對象的變量,被複制對象的所有變量都含有與原來的對象相同的值。那些引用其他對象的變量将指向被複制過的新對象,而不再是原有的那些被引用的對象。換言之,深複制把要複制的對象所引用的對象都複制了一遍。

如何實作克隆

我麼先不管深克隆、還是淺克隆。首先,要先了解如何實作克隆,實作克隆需要滿足以下三個步驟

  1. 對象的類實作Cloneable接口;
  2. 覆寫Object類的clone()方法(覆寫clone()方法,通路修飾符設為public,預設是protected,但是如果所有類都在同一個包下protected是可以通路的);
  3. 在clone()方法中調用super.clone();

實作一個克隆

先定義一個

score

類,表示分數資訊。

public class Score {
    private String category;
    private double fraction;

    public Score() {
    }

    public Score(String category, double fraction) {
        this.category = category;
        this.fraction = fraction;
    }

    //getter/setter省略

    @Override
    public String toString() {
        return "Score{" +
                "category='" + category + '\'' +
                ", fraction=" + fraction +
                '}';
    }
}
           

定義一個Person,其中包含

Score

屬性,來表示這個人的考試分數。

需要注意,Person類是實作了Cloneable接口的,并且重寫了

clone()

這個方法。

public class Person implements Cloneable{
    private String name;
    private int age;
    private List<Score> score;

    public Person() {
    }
    

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

克隆代碼測試,代碼邏輯不複雜,就是初始化一個對象

mic

,然後基于

mic

使用

clone

方法克隆出一個對象

dylan

接着通過修改被克隆對象

mic

的成員屬性,列印出這兩個對象的狀态資訊。

public class CloneMain {

    public static void main(String[] args) throws CloneNotSupportedException {
        Person mic=new Person();
        Score s1=new Score();
        s1.setCategory("國文");
        s1.setFraction(90);
        Score s2=new Score();
        s2.setCategory("數學");
        s2.setFraction(100);
        mic.setAge(18);
        mic.setName("Mic");
        mic.setScore(Arrays.asList(s1,s2));
        System.out.println("person對象初始化狀态:"+mic);

        Person dylan=(Person)mic.clone(); //克隆一個對象
        System.out.println("列印克隆對象:dylan:"+dylan);
        mic.setAge(20);
        mic.getScore().get(0).setFraction(70); //修改mic國文分數為70
        System.out.println("列印mic:"+mic);
        System.out.println("列印dylan:"+dylan);
    }
}
           

執行結果如下:

person對象初始化狀态:Person{name='Mic', age=18, score=[Score{category='國文', fraction=90.0}, Score{category='數學', fraction=100.0}]}
列印克隆對象:dylan:Person{name='Mic', age=18, score=[Score{category='國文', fraction=90.0}, Score{category='數學', fraction=100.0}]}
列印mic:Person{name='Mic', age=20, score=[Score{category='國文', fraction=70.0}, Score{category='數學', fraction=100.0}]}
列印dylan:Person{name='Mic', age=18, score=[Score{category='國文', fraction=70.0}, Score{category='數學', fraction=100.0}]}

           

從結果中可以發現:

  1. 修改

    mic

    對象本身的普通屬性

    age

    ,發現該屬性的修改隻影響到

    mic

    對象本身的執行個體。
  2. 當修改

    mic

    對象的國文成績時,

    dylan

    對象的國文成績也發生了變化。

為什麼會導緻這個現象?回過頭看一下淺克隆的定義:

淺克隆:建立一個新對象,新對象的屬性和原來對象完全相同,對于非基本類型屬性,仍指向原有屬性所指向的對象的記憶體位址

需要特别強調

非基本類型

,對于非基本類型,傳遞的是值,是以新的

dylan

對象會對該屬性建立一個副本。同樣,對于

final

修飾的屬性,由于它的不可變性,在淺克隆時,也會在記憶體中建立副本。

如圖所示,dylan對象從mic對象克隆過來後,dylan對象的記憶體位址指向的是同一個。是以當

mic

這個對象中的屬性發生變化時,

dylan

對象的屬性也會發生變化。

沒看這篇文章之前,我以為真的懂深克隆和淺克隆。

clone方法的源碼分析

經過上述案例示範可以發現,如果對象實作Cloneable并重寫clone方法不進行任何操作時,調用clone是進行的淺克隆,那clone方法是如何實作的呢?它預設情況下做了什麼?

clone方法是Object中預設提供的,它的源碼定義如下

protected native Object clone() throws CloneNotSupportedException;
           

從源碼中我們可以看到幾個關鍵點:

1.clone方法是native方法,native方法的效率遠高于非native方法,是以如果我們需要拷貝一個對象,建議使用clone,而不是new。

2.該方法被protected修飾。這就意味着想要使用,則必須重寫該方法,并且設定成public。

3.傳回值是一個Object對象,是以通過

clone

方法克隆一個對象,需要強制轉換。

4.如果在沒有實作Cloneable接口的執行個體上調用Object的clone()方法,則會導緻抛出CloneNotSupporteddException;

再來看一下Object.clone方法上的注釋,注釋的内容有點長。

/**
     * Creates and returns a copy of this object.  The precise meaning
     * of "copy" may depend on the class of the object. The general
     * intent is that, for any object {@code x}, the expression:
     * <blockquote>
     * <pre>
     * x.clone() != x</pre></blockquote>
     * will be true, and that the expression:
     * <blockquote>
     * <pre>
     * x.clone().getClass() == x.getClass()</pre></blockquote>
     * will be {@code true}, but these are not absolute requirements.
     * While it is typically the case that:
     * <blockquote>
     * <pre>
     * x.clone().equals(x)</pre></blockquote>
     * will be {@code true}, this is not an absolute requirement.
     * <p>
     * By convention, the returned object should be obtained by calling
     * {@code super.clone}.  If a class and all of its superclasses (except
     * {@code Object}) obey this convention, it will be the case that
     * {@code x.clone().getClass() == x.getClass()}.
     * <p>
     * By convention, the object returned by this method should be independent
     * of this object (which is being cloned).  To achieve this independence,
     * it may be necessary to modify one or more fields of the object returned
     * by {@code super.clone} before returning it.  Typically, this means
     * copying any mutable objects that comprise the internal "deep structure"
     * of the object being cloned and replacing the references to these
     * objects with references to the copies.  If a class contains only
     * primitive fields or references to immutable objects, then it is usually
     * the case that no fields in the object returned by {@code super.clone}
     * need to be modified.
     * <p>
     * The method {@code clone} for class {@code Object} performs a
     * specific cloning operation. First, if the class of this object does
     * not implement the interface {@code Cloneable}, then a
     * {@code CloneNotSupportedException} is thrown. Note that all arrays
     * are considered to implement the interface {@code Cloneable} and that
     * the return type of the {@code clone} method of an array type {@code T[]}
     * is {@code T[]} where T is any reference or primitive type.
     * Otherwise, this method creates a new instance of the class of this
     * object and initializes all its fields with exactly the contents of
     * the corresponding fields of this object, as if by assignment; the
     * contents of the fields are not themselves cloned. Thus, this method
     * performs a "shallow copy" of this object, not a "deep copy" operation.
     * <p>
     * The class {@code Object} does not itself implement the interface
     * {@code Cloneable}, so calling the {@code clone} method on an object
     * whose class is {@code Object} will result in throwing an
     * exception at run time.
     *
     * @return     a clone of this instance.
     * @throws  CloneNotSupportedException  if the object's class does not
     *               support the {@code Cloneable} interface. Subclasses
     *               that override the {@code clone} method can also
     *               throw this exception to indicate that an instance cannot
     *               be cloned.
     * @see java.lang.Cloneable
     */
    protected native Object clone() throws CloneNotSupportedException;
           

上述方法中的注釋描述中,對于

clone

方法關于複制描述,提出了三個規則,也就是說,”複制“的确切定義取決于對象本身,它可以滿足以下任意一條規則:

  • 對于所有對象,x.clone () !=x 應當傳回 true,因為克隆對象與原對象不是同一個對象。
  • 對于所有對象,x.clone ().getClass () == x.getClass () 應當傳回 true,因為克隆對象與原對象的類型是一樣的。
  • 對于所有對象,x.clone ().equals (x) 應當傳回 true,因為使用 equals 比較時,它們的值都是相同的。
是以,從clone方法的源碼中可以得到一個結論,clone方法是深克隆還是淺克隆,取決于實作克隆方法對象的本身實作。

深克隆

了解了淺克隆,我們就不難猜測到,所謂深克隆的本質,應該是如下圖所示。

沒看這篇文章之前,我以為真的懂深克隆和淺克隆。

dylan

這個對象執行個體從

mic

對象克隆之後,應該要配置設定一塊新的記憶體位址,進而實作在記憶體位址上的隔離。

深拷貝實作的是對所有可變(沒有被final修飾的引用變量)引用類型的成員變量都開辟獨立的記憶體空間,使得拷貝對象和被拷貝對象之間彼此獨立,是以一般深拷貝對于淺拷貝來說是比較耗費時間和記憶體開銷的。

深克隆實作

修改Person類中的

clone()

方法,代碼如下。

@Override
protected Object clone() throws CloneNotSupportedException {
  Person p=(Person)super.clone(); //可以直接使用clone方法克隆,因為String類型中的屬性是final修飾,而int是基本類型,都會建立副本
  if(this.score!=null&&this.score.size()>0){ //如果score不為空時,才做深度克隆
    //由于`score`是引用類型,是以需要重新配置設定記憶體空間
    List<Score> ls=new ArrayList<>();
    this.score.stream().forEach(score->{
      Score s=new Score();
      s.setFraction(score.getFraction());
      s.setCategory(score.getCategory());
      ls.add(s);
    });
    p.setScore(ls);
  }
  return p;
}
           

再次執行,運作結果如下

person對象初始化狀态:Person{name='Mic', age=18, score=[Score{category='國文', fraction=90.0}, Score{category='數學', fraction=100.0}]}
列印克隆對象:dylan:Person{name='Mic', age=18, score=[Score{category='國文', fraction=90.0}, Score{category='數學', fraction=100.0}]}
列印mic:Person{name='Mic', age=20, score=[Score{category='國文', fraction=70.0}, Score{category='數學', fraction=100.0}]}
列印dylan:Person{name='Mic', age=18, score=[Score{category='國文', fraction=90.0}, Score{category='數學', fraction=100.0}]}

Process finished with exit code 0

           

從結果可以看到,這兩個對象之間并沒有互相影響,因為我們在

clone

方法中,對于

Person

這個類的成員屬性

Score

new

建立了一個新的對象,這樣就使得兩個對象分别指向不同的記憶體位址。

建立一個新對象,屬性中引用的其他對象也會被克隆,不再指向原有對象位址。總之深淺克隆都會在堆中新配置設定一塊區域,差別在于對象屬性引用的對象是否需要進行克隆(遞歸性的)

深克隆的其他實作方式

深克隆的實作方式很多,總的來說有以下幾種:

  • 所有對象都實作克隆方法。
  • 通過構造方法實作深克隆。
  • 使用 JDK 自帶的位元組流。
  • 使用第三方工具實作,比如:Apache Commons Lang。
  • 使用 JSON 工具類實作,比如:Gson,FastJSON 等等。
其實,深克隆既然是在記憶體中建立新的對象,那麼任何能夠建立新執行個體對象的方式都能完成這個動作,是以不局限于這些方法。

所有對象都實作克隆方法

由于淺克隆本質上是因為引用對象指向同一塊記憶體位址,如果每個對象都實作克隆方法,意味着每個對象的最基本機關是基本資料類型或者封裝類型,而這些類型在克隆時會建立副本,進而避免了指向同一塊記憶體位址的問題。

修改代碼如下。

public class Person implements Cloneable {
    private String name;
    private int age;
    private List<Score> score;

    public Person() {
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        Person p=(Person)super.clone();
        if(this.score!=null&&this.score.size()>0){ //如果score不為空時,才做深度克隆
            //由于`score`是引用類型,是以需要重新配置設定記憶體空間
            List<Score> ls=new ArrayList<>();
            this.score.stream().forEach(score->{
                try {
                    ls.add((Score)score.clone()); //這裡用了克隆方法
                } catch (CloneNotSupportedException e) {
                    e.printStackTrace();
                }
            });
            p.setScore(ls);
        }
        return p;
    }
}
           

修改Score對象

public class Score implements Cloneable {
    private String category;
    private double fraction;

    public Score() {
    }

    public Score(String category, double fraction) {
        this.category = category;
        this.fraction = fraction;
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}
           
Person dylan=(Person)mic.clone(); //克隆一個對象
           

運作結果如下

person對象初始化狀态:Person{name='Mic', age=18, score=[Score{category='國文', fraction=90.0}, Score{category='數學', fraction=100.0}]}
列印克隆對象:dylan:Person{name='Mic', age=18, score=[Score{category='國文', fraction=90.0}, Score{category='數學', fraction=100.0}]}
列印mic:Person{name='Mic', age=20, score=[Score{category='國文', fraction=70.0}, Score{category='數學', fraction=100.0}]}
列印dylan:Person{name='Mic', age=18, score=[Score{category='國文', fraction=90.0}, Score{category='數學', fraction=100.0}]}
           

構造方法實作深克隆,其實是我們經常使用的方法,就是使用

new

關鍵字來執行個體化一個新的對象,然後通過構造參數傳值來實作資料拷貝。

public class Person implements Cloneable {
    private String name;
    private int age;
    private List<Score> score;

    public Person() {
    }

    public Person(String name, int age, List<Score> score) {
        this.name = name;
        this.age = age;
        this.score = score;
    }
}
           

克隆的時候,我們這麼做

Person dylan=new Person(mic.getName(),mic.getAge(),mic.getScore()); //克隆一個對象
           

基于ObjectStream實作深克隆

在Java中,對象流也可以實作深克隆,大家可能對對象流這個名詞有點陌生,它的定義如下:

  • ObjectOutputStream, 對象輸出流,把一個對象轉換為二進制格式資料
  • ObjectInputStream,對象輸入流,把一個二進制資料轉換為對象。

這兩個對象,在Java中通常用來實作對象的序列化。

建立一個工具類,使用ObjectStream來實作對象的克隆,代碼實作邏輯不難:

  1. 使用ObjectOutputStream,把一個對象轉換為資料流存儲到對象ByteArrayOutputStream中。
  2. 再從記憶體中讀取該資料流,使用ObjectInputStream,把該資料流轉換為目标對象。
public class ObjectStreamClone {

    public static <T extends Serializable> T clone(T t){
        T cloneObj = null;
        try {
            // bo,存儲對象輸出流,寫入到記憶體
            ByteArrayOutputStream bo = new ByteArrayOutputStream();
            //對象輸出流,把對象轉換為資料流
            ObjectOutputStream oos = new ObjectOutputStream(bo);
            oos.writeObject(t);
            oos.close();
            // 配置設定記憶體,寫入原始對象,生成新對象
            ByteArrayInputStream bi = new ByteArrayInputStream(bo.toByteArray());
            ObjectInputStream oi = new ObjectInputStream(bi);
            // 傳回生成的新對象
            cloneObj = (T) oi.readObject();
            oi.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return cloneObj;
    }

}
           

Person對象和Score對象均需要實作Serializable接口,

public class Person implements Serializable {
}
public class Score implements Serializable {}
           

修改測試類的克隆方法.

Person dylan=(Person)ObjectStreamClone.clone(mic); //克隆一個對象
           

運作結果如下:

person對象初始化狀态:Person{name='Mic', age=18, score=[Score{category='國文', fraction=90.0}, Score{category='數學', fraction=100.0}]}
列印克隆對象:dylan:Person{name='Mic', age=18, score=[Score{category='國文', fraction=90.0}, Score{category='數學', fraction=100.0}]}
列印mic:Person{name='Mic', age=20, score=[Score{category='國文', fraction=70.0}, Score{category='數學', fraction=100.0}]}
列印dylan:Person{name='Mic', age=18, score=[Score{category='國文', fraction=90.0}, Score{category='數學', fraction=100.0}]}
           

通過對象流能夠實作深克隆,其根本原因還是在于對象的序列化之後,已經脫離了JVM記憶體對象的範疇,畢竟一個對象序列化之後,是可以通過檔案、或者網絡跨JVM傳輸的,是以對象在反序列化時,必然需要基于該資料流重新反射生成新的對象。

問題解答

問題:深克隆和淺克隆的實作方式

回答:

  1. 淺克隆是指被複制對象中屬于引用類型的成員變量的記憶體位址和被克隆對象的記憶體位址相同,也就是克隆對象隻實作了對被克隆對象基本類型的副本克隆。

    淺克隆的實作方式,可以實作Cloneable接口,并重寫clone方法,即可完成淺克隆。

    淺克隆的好處是,避免了引用對象的記憶體配置設定和回收,提高對象的複制效率。

  2. 深克隆時指實作對于基本類型和引用類型的完整克隆,克隆對象和被克隆對象中的引用對象的記憶體位址完全隔離。

    深克隆的實作方式:

    • 基于Cloneable接口重寫clone方法,但是我們需要在clone方法中,針對應用類型的成員變量,使用

      new

      關鍵字配置設定獨立的記憶體空間。
    • 基于Java中對象流的方式實作
    • 基于構造方法實作深度克隆
    • 被克隆的對象中所有涉及到引用類型變量的對象,全部實作克隆方法,并且在被克隆對象的clone方法中,需要調用所有成員對象的clone方法實作對象克隆

問題總結

深克隆的本質,其實是保證被克隆對象中所有應用對象以及引用所嵌套的引用對象,全部配置設定一塊獨立的記憶體空間,避免克隆對象和被克隆對象指向同一塊記憶體位址,造成資料錯誤等問題。

是以,深克隆,表示對象拷貝的深度,因為在Java中對象的嵌套是非常常見的。了解了這個知識點,才能避免在開發過程中遇到一些奇奇怪怪的問題。

關注[跟着Mic學架構]公衆号,擷取更多精品原創

沒看這篇文章之前,我以為真的懂深克隆和淺克隆。

繼續閱讀