Java 源碼剖析(07)--簡述深克隆和淺克隆


使用克隆可以爲我們快速地構建出一個已有對象的副本,它屬於 Java 基礎的一部分,什麼是淺克隆和深克隆?如何實現克隆?

1)深克隆和淺克隆的區別

淺克隆(Shadow Clone)是把原型對象中成員變量爲值類型的屬性都複製給克隆對象,把原型對象中成員變量爲引用類型的引用地址也複製給克隆對象,也就是原型對象中如果有成員變量爲引用對象,則此引用對象的地址是共享給原型對象和克隆對象的。簡單來說就是淺克隆只會複製原型對象,但不會複製它所引用的對象,如下圖所示:
在這裏插入圖片描述
深克隆(Deep Clone)是將原型對象中的所有類型,無論是值類型還是引用類型,都複製一份給克隆對象,也就是說深克隆會把原型對象和原型對象所引用的對象,都複製一份給克隆對象,如下圖所示:
在這裏插入圖片描述
在 Java 語言中要實現克隆則需要實現 Cloneable 接口,並重寫 Object 類中的 clone() 方法,實現代碼如下:

public class CloneExample {
    public static void main(String[] args) throws CloneNotSupportedException {
        // 創建被賦值對象
        People p1 = new People();
        p1.setId(1);
        p1.setName("Java");
        // 克隆 p1 對象
        People p2 = (People) p1.clone();
        // 打印名稱
        System.out.println("p2:" + p2.getName());
    }
    static class People implements Cloneable {
        // 屬性
        private Integer id;
        private String name;
        /**
         * 重寫 clone 方法
         * @throws CloneNotSupportedException
         */
        @Override
        protected Object clone() throws CloneNotSupportedException {
            return super.clone();
        }
        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;
        }
    }
}

以上程序執行的結果爲:

p2:Java

2)知識擴展

2.1)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>
 * ......
 */
protected native Object clone() throws CloneNotSupportedException;

從以上源碼的註釋信息中我們可以看出,Object 對 clone() 方法的約定有三條:

  • 對於所有對象來說,x.clone() !=x 應當返回 true,因爲克隆對象與原對象不是同一個對象;
  • 對於所有對象來說,x.clone().getClass() == x.getClass() 應當返回true,因爲克隆對象與原對象的類型是一樣的;
  • 對於所有對象來說,x.clone().equals(x) 應當返回 true,因爲使用 equals 比較時,它們的值都是相同的。

除了註釋信息外,我們看 clone() 的實現方法,發現 clone() 是使用 native 修飾的本地方法,因此執行的性能會很高,並且它返回的類型爲 Object,因此在調用克隆之後要把對象強轉爲目標類型纔行。

2.2)Arrays.copyOf()

如果是數組類型,我們可以直接使用 Arrays.copyOf() 來實現克隆,實現代碼如下:

People[] o1 = {new People(1, "Java")};
People[] o2 = Arrays.copyOf(o1, o1.length);
// 修改原型對象的第一個元素的值
o1[0].setName("Jdk");
System.out.println("o1:" + o1[0].getName());
System.out.println("o2:" + o2[0].getName());

以上程序的執行結果爲:

o1:Jdk
o2:Jdk

從結果可以看出,我們在修改克隆對象的第一個元素之後,原型對象的第一個元素也跟着被修改了,這說明 Arrays.copyOf() 其實是一個淺克隆。

因爲數組比較特殊數組本身就是引用類型,因此在使用 Arrays.copyOf() 其實只是把引用地址複製了一份給克隆對象,如果修改了它的引用對象,那麼指向它的(引用地址)所有對象都會發生改變,因此看到的結果是,修改了克隆對象的第一個元素,原型對象也跟着被修改了。

2.3)深克隆實現方式彙總

深克隆的實現方式有很多種,大體可以分爲以下幾類:

  • 所有對象都實現克隆方法;
  • 通過構造方法實現深克隆;
  • 使用 JDK 自帶的字節流實現深克隆;
  • 使用第三方工具實現深克隆,比如 Apache Commons Lang;
  • 使用 JSON 工具類實現深克隆,比如 Gson、FastJSON 等。

接下來我們分別來實現以上這些方式,在開始之前先定義一個公共的用戶類,代碼如下:

/**
 * 用戶類
 */
public class People {
    private Integer id;
    private String name;
    private Address address; // 包含 Address 引用對象
    // 忽略構造方法、set、get 方法
}
/**
 * 地址類
 */
public class Address {
    private Integer id;
    private String city;
    // 忽略構造方法、set、get 方法
}

可以看出在 People 對象中包含了一個引用對象 Address。

2.3.1)所有對象都實現克隆

這種方式我們需要修改 People 和 Address 類,讓它們都實現 Cloneable 的接口,讓所有的引用對象都實現克隆,從而實現 People 類的深克隆,代碼如下:

public class CloneExample {
    public static void main(String[] args) throws CloneNotSupportedException {
          // 創建被賦值對象
          Address address = new Address(110, "北京");
          People p1 = new People(1, "Java", address);
          // 克隆 p1 對象
          People p2 = p1.clone();
          // 修改原型對象
          p1.getAddress().setCity("西安");
          // 輸出 p1 和 p2 地址信息
          System.out.println("p1:" + p1.getAddress().getCity() +
                  " p2:" + p2.getAddress().getCity());
    }
    /**
     * 用戶類
     */
    static class People implements Cloneable {
        private Integer id;
        private String name;
        private Address address;
        /**
         * 重寫 clone 方法
         * @throws CloneNotSupportedException
         */
        @Override
        protected People clone() throws CloneNotSupportedException {
            People people = (People) super.clone();
            people.setAddress(this.address.clone()); // 引用類型克隆賦值
            return people;
        }
        // 忽略構造方法、set、get 方法
    }
    /**
     * 地址類
     */
    static class Address implements Cloneable {
        private Integer id;
        private String city;
        /**
         * 重寫 clone 方法
         * @throws CloneNotSupportedException
         */
        @Override
        protected Address clone() throws CloneNotSupportedException {
            return (Address) super.clone();
        }
        // 忽略構造方法、set、get 方法
    }
}

以上程序的執行結果爲:

p1:西安 p2:北京

從結果可以看出,當我們修改了原型對象的引用屬性之後,並沒有影響克隆對象,這說明此對象已經實現了深克隆。

2.3.2)通過構造方法實現深克隆

《Effective Java》 中推薦使用構造器(Copy Constructor)來實現深克隆,如果構造器的參數爲基本數據類型或字符串類型則直接賦值,如果是對象類型,則需要重新 new 一個對象,實現代碼如下:

public class SecondExample {
    public static void main(String[] args) throws CloneNotSupportedException {
        // 創建對象
        Address address = new Address(110, "北京");
        People p1 = new People(1, "Java", address);

        // 調用構造函數克隆對象
        People p2 = new People(p1.getId(), p1.getName(),
                new Address(p1.getAddress().getId(), p1.getAddress().getCity()));

        // 修改原型對象
        p1.getAddress().setCity("西安");

        // 輸出 p1 和 p2 地址信息
        System.out.println("p1:" + p1.getAddress().getCity() +
                " p2:" + p2.getAddress().getCity());
    }

    /**
     * 用戶類
     */
    static class People {
        private Integer id;
        private String name;
        private Address address;
        // 忽略構造方法、set、get 方法
    }

    /**
     * 地址類
     */
    static class Address {
        private Integer id;
        private String city;
        // 忽略構造方法、set、get 方法
    }
}

以上程序的執行結果爲:

p1:西安 p2:北京

從結果可以看出,當我們修改了原型對象的引用屬性之後,並沒有影響克隆對象,這說明此對象已經實現了深克隆。

2.3.3)通過字節流實現深克隆

通過 JDK 自帶的字節流實現深克隆的方式,是先將要原型對象寫入到內存中的字節流,然後再從這個字節流中讀出剛剛存儲的信息,來作爲一個新的對象返回,那麼這個新對象和原型對象就不存在任何地址上的共享,這樣就實現了深克隆,代碼如下:

import java.io.*;

public class ThirdExample {
    public static void main(String[] args) throws CloneNotSupportedException {
        // 創建對象
        Address address = new Address(110, "北京");
        People p1 = new People(1, "Java", address);

        // 通過字節流實現克隆
        People p2 = (People) StreamClone.clone(p1);

        // 修改原型對象
        p1.getAddress().setCity("西安");

        // 輸出 p1 和 p2 地址信息
        System.out.println("p1:" + p1.getAddress().getCity() +
                " p2:" + p2.getAddress().getCity());
    }

    /**
     * 通過字節流實現克隆
     */
    static class StreamClone {
        public static <T extends Serializable> T clone(People obj) {
            T cloneObj = null;
            try {
                // 寫入字節流
                ByteArrayOutputStream bo = new ByteArrayOutputStream();
                ObjectOutputStream oos = new ObjectOutputStream(bo);
                oos.writeObject(obj);
                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;
        }
    }

    /**
     * 用戶類
     */
    static class People implements Serializable {
        private Integer id;
        private String name;
        private Address address;
        // 忽略構造方法、set、get 方法
    }

    /**
     * 地址類
     */
    static class Address implements Serializable {
        private Integer id;
        private String city;
        // 忽略構造方法、set、get 方法
    }
}

以上程序的執行結果爲:

p1:西安 p2:北京

此方式需要注意的是,由於是通過字節流序列化實現的深克隆,因此每個對象必須能被序列化,必須實現 Serializable 接口,標識自己可以被序列化,否則會拋出異常 (java.io.NotSerializableException)。

2.3.4)通過第三方工具實現深克隆

本課時使用 Apache Commons Lang 來實現深克隆,實現代碼如下:

import org.apache.commons.lang3.SerializationUtils;

import java.io.Serializable;

/**
 * 深克隆實現方式四:通過 apache.commons.lang 實現
 */
public class FourthExample {
    public static void main(String[] args) throws CloneNotSupportedException {
        // 創建對象
        Address address = new Address(110, "北京");
        People p1 = new People(1, "Java", address);

        // 調用 apache.commons.lang 克隆對象
        People p2 = (People) SerializationUtils.clone(p1);

        // 修改原型對象
        p1.getAddress().setCity("西安");

        // 輸出 p1 和 p2 地址信息
        System.out.println("p1:" + p1.getAddress().getCity() +
                " p2:" + p2.getAddress().getCity());
    }

    /**
     * 用戶類
     */
    static class People implements Serializable {
        private Integer id;
        private String name;
        private Address address;
        // 忽略構造方法、set、get 方法
    }

    /**
     * 地址類
     */
    static class Address implements Serializable {
        private Integer id;
        private String city;
        // 忽略構造方法、set、get 方法
    }
}

以上程序的執行結果爲:

p1:西安 p2:北京

可以看出此方法和第三種實現方式類似,都需要實現 Serializable 接口,都是通過字節流的方式實現的,只不過這種實現方式是第三方提供了現成的方法,讓我們可以直接調用。

2.3.5)通過 JSON 工具類實現深克隆

本課時我們使用 Google 提供的 JSON 轉化工具 Gson 來實現,其他 JSON 轉化工具類也是類似的,實現代碼如下:

import com.google.gson.Gson;

/**
 * 深克隆實現方式五:通過 JSON 工具實現
 */
public class FifthExample {
    public static void main(String[] args) throws CloneNotSupportedException {
        // 創建對象
        Address address = new Address(110, "北京");
        People p1 = new People(1, "Java", address);

        // 調用 Gson 克隆對象
        Gson gson = new Gson();
        People p2 = gson.fromJson(gson.toJson(p1), People.class);

        // 修改原型對象
        p1.getAddress().setCity("西安");

        // 輸出 p1 和 p2 地址信息
        System.out.println("p1:" + p1.getAddress().getCity() +
                " p2:" + p2.getAddress().getCity());
    }

    /**
     * 用戶類
     */
    static class People {
        private Integer id;
        private String name;
        private Address address;
        // 忽略構造方法、set、get 方法
    }

    /**
     * 地址類
     */
    static class Address {
        private Integer id;
        private String city;
        // 忽略構造方法、set、get 方法
    }
}

以上程序的執行結果爲:

p1:西安 p2:北京

使用 JSON 工具類會先把對象轉化成字符串,再從字符串轉化成新的對象,因爲新對象是從字符串轉化而來的,因此不會和原型對象有任何的關聯,這樣就實現了深克隆,其他類似的 JSON 工具類實現方式也是一樣的。

3)克隆設計理念猜想

對於克隆爲什麼要這樣設計,官方沒有直接給出答案,只能憑藉一些經驗和源碼文檔來試着回答一下這個問題。Java 中實現克隆需要兩個主要的步驟,一是 實現 Cloneable 空接口,二是重寫 Object 的 clone() 方法再調用父類的克隆方法 (super.clone(),那爲什麼要這麼做?

從源碼中可以看出 Cloneable 接口誕生的比較早,JDK 1.0 就已經存在了,因此從那個時候就已經有克隆方法了,那我們怎麼來標識一個類級別對象擁有克隆方法呢?克隆雖然重要,但我們不能給每個類都默認加上克隆,這顯然是不合適的,那我們能使用的手段就只有這幾個了:

在類上新增標識,此標識用於聲明某個類擁有克隆的功能,像 final 關鍵字一樣;

  • 使用 Java 中的註解;
  • 實現某個接口;
  • 繼承某個類。

先說第一個,爲了一個重要但不常用的克隆功能, 單獨新增一個類標識,這顯然不合適;再說第二個,因爲克隆功能出現的比較早,那時候還沒有註解功能,因此也不能使用;第三點基本滿足我們的需求,第四點和第一點比較類似,爲了一個克隆功能需要犧牲一個基類,並且 Java 只能單繼承,因此這個方案也不合適。採用排除法,無疑使用實現接口的方式是那時最合理的方案了,而且在 Java 語言中一個類可以實現多個接口。

爲什麼要在 Object 中添加一個 clone() 方法呢?

因爲 clone() 方法語義的特殊性,因此最好能有 JVM 的直接支持,既然要 JVM 直接支持,就要找一個 API 來把這個方法暴露出來纔行,最直接的做法就是把它放入到一個所有類的基類 Object 中,這樣所有類就可以很方便地調用到了。

4)小結

本文講解了淺克隆和深克隆的概念,以及 Object 對 clone() 方法的約定;還演示了數組的 copyOf() 方法其實爲淺克隆,以及深克隆的 5 種實現方式;最後講了 Java 語言中克隆的設計思路猜想。
——————————————————————————————————————————————
關注公衆號,回覆 【算法】,獲取高清算法書!
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章