Effective Java 第三版——13. 謹慎地重寫 clone 方法


Tips
《Effective Java, Third Edition》一書英文版已經出版,這本書的第二版想必很多人都讀過,號稱Java四大名著之一,不過第二版2009年出版,到現在已經將近8年的時間,但隨着Java 6,7,8,甚至9的發布,Java語言發生了深刻的變化。
在這里第一時間翻譯成中文版。供大家學習分享之用。

Effective Java, Third Edition

13. 謹慎地重寫 clone 方法

Cloneable接口的目的是作為一個mixin接口(條目 20),公布這樣的類允許克隆。不幸的是,它沒有達到這個目的。它的主要缺點是缺少clone方法,而Object的clone方法是受保護的。你不能,不借助反射(條目 65),僅僅因為它實現了Cloneable接口,就調用對象上的 clone 方法。即使是反射調用也可能失敗,因為不能保證對象具有可訪問的 clone方法。盡管存在許多缺陷,該機制在合理的范圍內使用,所以理解它是值得的。這個條目告訴你如何實現一個行為良好的 clone方法,在適當的時候討論這個方法,並提出替代方案。

既然Cloneable接口不包含任何方法,那它用來做什么? 它決定了Object的受保護的clone 方法實現的行為:如果一個類實現了Cloneable接口,那么Object的clone方法將返回該對象的逐個屬性(field-by-field)拷貝;否則會拋出CloneNotSupportedException異常。這是一個非常反常的接口使用,而不應該被效仿。 通常情況下,實現一個接口用來表示可以為客戶做什么。但對於Cloneable接口,它會修改父類上受保護方法的行為。

雖然規范並沒有說明,但在實踐中,實現Cloneable接口的類希望提供一個正常運行的公共 clone方法。為了實現這一目標,該類及其所有父類必須遵循一個復雜的、不可執行的、稀疏的文檔協議。由此產生的機制是脆弱的、危險的和不受語言影響的(extralinguistic):它創建對象而不需要調用構造方法。

clone方法的通用規范很薄弱的。 以下內容是從 Object 規范中復制出來的:

創建並返回此對象的副本。 “復制(copy)”的確切含義可能取決於對象的類。 一般意圖是,對於任何對象x,表達式x.clone() != x返回 true,並且x.clone().getClass() == x.getClass()也返回 true,但它們不是絕對的要求,但通常情況下,x.clone().equals(x)返回 true,當然這個要求也不是絕對的。

根據約定,這個方法返回的對象應該通過調用super.clone方法獲得的。 如果一個類和它的所有父類(Object除外)都遵守這個約定,情況就是如此,x.clone().getClass() == x.getClass()

根據約定,返回的對象應該獨立於被克隆的對象。 為了實現這種獨立性,在返回對象之前,可能需要修改由super.clone返回的對象的一個或多個屬性。

這種機制與構造方法鏈(chaining)很相似,只是它沒有被強制執行;如果一個類的clone方法返回一個通過調用構造方法獲得而不是通過調用super.clone的實例,那么編譯器不會抱怨,但是如果一個類的子類調用了super.clone,那么返回的對象包含錯誤的類,從而阻止子類 clone 方法正常執行。如果一個類重寫的 clone 方法是有 final 修飾的,那么這個約定可以被安全地忽略,因為子類不需要擔心。但是,如果一個final類有一個不調用super.clone的clone方法,那么這個類沒有理由實現Cloneable接口,因為它不依賴於Object的clone實現的行為。

假設你希望在一個類中實現Cloneable接口,它的父類提供了一個行為良好的 clone方法。首先調用super.clone。 得到的對象將是原始的完全功能的復制品。 在你的類中聲明的任何屬性將具有與原始屬性相同的值。 如果每個屬性包含原始值或對不可變對象的引用,則返回的對象可能正是你所需要的,在這種情況下,不需要進一步的處理。 例如,對於條目 11中的PhoneNumber類,情況就是這樣,但是請注意,不可變類永遠不應該提供clone方法,因為這只會浪費復制。 有了這個警告,以下是PhoneNumber類的clone方法:

// Clone method for class with no references to mutable state
@Override public PhoneNumber clone() {
    try {
        return (PhoneNumber) super.clone();
    } catch (CloneNotSupportedException e) {
        throw new AssertionError();  // Can't happen
    }
}

為了使這個方法起作用,PhoneNumber的類聲明必須被修改,以表明它實現了Cloneable接口。 雖然Object類的clone方法返回Object類,但是這個clone方法返回PhoneNumber類。 這樣做是合法和可取的,因為Java支持協變返回類型。 換句話說,重寫方法的返回類型可以是重寫方法的返回類型的子類。 這消除了在客戶端轉換的需要。 在返回之前,我們必須將Object的super.clone的結果強制轉換為PhoneNumber,但保證強制轉換成功。

super.clone的調用包含在一個try-catch塊中。 這是因為Object聲明了它的clone方法來拋出CloneNotSupportedException異常,這是一個檢查時異常。 由於PhoneNumber實現了Cloneable接口,所以我們知道調用super.clone會成功。 這里引用的需要表明CloneNotSupportedException應該是未被檢查的(條目 71)。

如果對象包含引用可變對象的屬性,則前面顯示的簡單clone實現可能是災難性的。 例如,考慮條目 7中的Stack類:

public class Stack {

    private Object[] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;

    public Stack() {
        this.elements = new Object[DEFAULT_INITIAL_CAPACITY];
    }

    public void push(Object e) {
        ensureCapacity();
        elements[size++] = e;
    }

    public Object pop() {
        if (size == 0)
            throw new EmptyStackException();
        Object result = elements[--size];

        elements[size] = null; // Eliminate obsolete reference
        return result;
    }

    // Ensure space for at least one more element.
    private void ensureCapacity() {
        if (elements.length == size)
            elements = Arrays.copyOf(elements, 2 * size + 1);
    }
}

假設你想讓這個類可以克隆。 如果clone方法僅返回super.clone()調用的對象,那么生成的Stack實例在其size 屬性中具有正確的值,但elements屬性引用與原始Stack實例相同的數組。 修改原始實例將破壞克隆中的不變量,反之亦然。 你會很快發現你的程序產生了無意義的結果,或者拋出NullPointerException異常。

這種情況永遠不會發生,因為調用Stack類中的唯一構造方法。 實際上,clone方法作為另一種構造方法; 必須確保它不會損壞原始對象,並且可以在克隆上正確建立不變量。 為了使Stack上的clone方法正常工作,它必須復制stack 對象的內部。 最簡單的方法是對元素數組遞歸調用clone方法:

// Clone method for class with references to mutable state
@Override public Stack clone() {
    try {
        Stack result = (Stack) super.clone();
        result.elements = elements.clone();
        return result;
    } catch (CloneNotSupportedException e) {
        throw new AssertionError();
    }
}

請注意,我們不必將elements.clone的結果轉換為Object[]數組。 在數組上調用clone會返回一個數組,其運行時和編譯時類型與被克隆的數組相同。 這是復制數組的首選習語。 事實上,數組是clone 機制的唯一有力的用途。

還要注意,如果elements屬性是final的,則以前的解決方案將不起作用,因為克隆將被禁止向該屬性分配新的值。 這是一個基本的問題:像序列化一樣,Cloneable體系結構與引用可變對象的final 屬性的正常使用不兼容,除非可變對象可以在對象和其克隆之間安全地共享。 為了使一個類可以克隆,可能需要從一些屬性中移除 final修飾符。

僅僅遞歸地調用clone方法並不總是足夠的。 例如,假設您正在為哈希表編寫一個clone方法,其內部包含一個哈希桶數組,每個哈希桶都指向“鍵-值”對鏈表的第一項。 為了提高性能,該類實現了自己的輕量級單鏈表,而沒有使用java內部提供的java.util.LinkedList:

public class HashTable implements Cloneable {
    private Entry[] buckets = ...;
    private static class Entry {
        final Object key;
        Object value;
        Entry  next;

        Entry(Object key, Object value, Entry next) {
            this.key   = key;
            this.value = value;
            this.next  = next;  
        }
    }
    ... // Remainder omitted
}

假設你只是遞歸地克隆哈希桶數組,就像我們為Stack所做的那樣:

// Broken clone method - results in shared mutable state!
@Override public HashTable clone() {
    try {
        HashTable result = (HashTable) super.clone();
        result.buckets = buckets.clone();
        return result;
    } catch (CloneNotSupportedException e) {
        throw new AssertionError();
    }
}

雖然被克隆的對象有自己的哈希桶數組,但是這個數組引用與原始數組相同的鏈表,這很容易導致克隆對象和原始對象中的不確定性行為。 要解決這個問題,你必須復制包含每個桶的鏈表。 下面是一種常見的方法:

// Recursive clone method for class with complex mutable state
public class HashTable implements Cloneable {
    private Entry[] buckets = ...;

    private static class Entry {
        final Object key;
        Object value;
        Entry  next;

        Entry(Object key, Object value, Entry next) {
            this.key   = key;
            this.value = value;
            this.next  = next;  
        }

        // Recursively copy the linked list headed by this Entry
        Entry deepCopy() {
            return new Entry(key, value,
                next == null ? null : next.deepCopy());
        }
    }

    @Override public HashTable clone() {
        try {
            HashTable result = (HashTable) super.clone();
            result.buckets = new Entry[buckets.length];
            for (int i = 0; i < buckets.length; i++)
                if (buckets[i] != null)
                    result.buckets[i] = buckets[i].deepCopy();
            return result;
        } catch (CloneNotSupportedException e) {
            throw new AssertionError();
        }
    }
    ... // Remainder omitted
}

私有類HashTable.Entry已被擴充以支持“深度復制”方法。 HashTable上的clone方法分配一個合適大小的新哈希桶數組,迭代原來哈希桶數組,深度復制每個非空的哈希桶。 Entry上的deepCopy方法遞歸地調用它自己以復制由頭節點開始的整個鏈表。 如果哈希桶不是太長,這種技術很聰明並且工作正常。但是,克隆鏈表不是一個好方法,因為它為列表中的每個元素消耗一個棧幀(stack frame)。 如果列表很長,這很容易導致堆棧溢出。 為了防止這種情況發生,可以用迭代來替換deepCopy中的遞歸:

// Iteratively copy the linked list headed by this Entry
Entry deepCopy() {
   Entry result = new Entry(key, value, next);
   for (Entry p = result; p.next != null; p = p.next)
      p.next = new Entry(p.next.key, p.next.value, p.next.next);
   return result;
}

克隆復雜可變對象的最后一種方法是調用super.clone,將結果對象中的所有屬性設置為其初始狀態,然后調用更高級別的方法來重新生成原始對象的狀態。 以HashTable為例,bucket屬性將被初始化為一個新的bucket數組,並且 put(key, value)方法(未示出)被調用用於被克隆的哈希表中的鍵值映射。 這種方法通常產生一個簡單,合理的優雅clone方法,其運行速度不如直接操縱克隆內部的方法快。 雖然這種方法是干凈的,但它與整個Cloneable體系結構是對立的,因為它會盲目地重寫構成體系結構基礎的逐個屬性對象復制。

與構造方法一樣,clone 方法絕對不可以在構建過程中,調用一個可以重寫的方法(條目 19)。如果 clone 方法調用一個在子類中重寫的方法,則在子類有機會在克隆中修復它的狀態之前執行該方法,很可能導致克隆和原始對象的損壞。因此,我們在前面討論的 put(key, value)方法應該時 final 或 private 修飾的。(如果時 private 修飾,那么大概是一個非 final 公共方法的輔助方法)。

Object 類的 clone方法被聲明為拋出CloneNotSupportedException異常,但重寫方法時不需要。 公共clone方法應該省略throws子句,因為不拋出檢查時異常的方法更容易使用(條目 71)。

在為繼承設計一個類時(條目 19),通常有兩種選擇,但無論選擇哪一種,都不應該實現 Clonable 接口。你可以選擇通過實現正確運行的受保護的 clone方法來模仿Object的行為,該方法聲明為拋出CloneNotSupportedException異常。 這給了子類實現Cloneable接口的自由,就像直接繼承Object一樣。 或者,可以選擇不實現工作的 clone方法,並通過提供以下簡並clone實現來阻止子類實現它:

// clone method for extendable class not supporting Cloneable
@Override
protected final Object clone() throws CloneNotSupportedException {
    throw new CloneNotSupportedException();
}

還有一個值得注意的細節。 如果你編寫一個實現了Cloneable的線程安全的類,記得它的clone方法必須和其他方法一樣(條目 78)需要正確的同步。 Object 類的clone方法是不同步的,所以即使它的實現是令人滿意的,也可能需要編寫一個返回super.clone()的同步clone方法。

回顧一下,實現Cloneable的所有類應該重寫公共clone方法,而這個方法的返回類型是類本身。 這個方法應該首先調用super.clone,然后修復任何需要修復的屬性。 通常,這意味着復制任何包含內部“深層結構”的可變對象,並用指向新對象的引用來代替原來指向這些對象的引用。雖然這些內部拷貝通常可以通過遞歸調用clone來實現,但這並不總是最好的方法。 如果類只包含基本類型或對不可變對象的引用,那么很可能是沒有屬性需要修復的情況。 這個規則也有例外。 例如,表示序列號或其他唯一ID的屬性即使是基本類型的或不可變的,也需要被修正。

這么復雜是否真的有必要?很少。 如果你繼承一個已經實現了Cloneable接口的類,你別無選擇,只能實現一個行為良好的clone方法。 否則,通常你最好提供另一種對象復制方法。 對象復制更好的方法是提供一個復制構造方法或復制工廠。 復制構造方法接受參數,其類型為包含此構造方法的類,例如,

// Copy constructor
public Yum(Yum yum) { ... };

復制工廠類似於復制構造方法的靜態工廠:

// Copy factory
public static Yum newInstance(Yum yum) { ... };

復制構造方法及其靜態工廠變體與Cloneable/clone相比有許多優點:它們不依賴風險很大的語言外的對象創建機制;不要求遵守那些不太明確的慣例;不會與final 屬性的正確使用相沖突; 不會拋出不必要的檢查異常; 而且不需要類型轉換。

此外,復制構造方法或復制工廠可以接受類型為該類實現的接口的參數。 例如,按照慣例,所有通用集合實現都提供了一個構造方法,其參數的類型為Collection或Map。 基於接口的復制構造方法和復制工廠(更適當地稱為轉換構造方法和轉換工廠)允許客戶端選擇復制的實現類型,而不是強制客戶端接受原始實現類型。 例如,假設你有一個HashSet,並且你想把它復制為一個TreeSet。 clone方法不能提供這種功能,但使用轉換構造方法很容易:new TreeSet<>(s)

考慮到與Cloneable接口相關的所有問題,新的接口不應該繼承它,新的可擴展類不應該實現它。 雖然實現Cloneable接口對於final類沒有什么危害,但應該將其視為性能優化的角度,僅在極少數情況下才是合理的(條目67)。 通常,復制功能最好由構造方法或工廠提供。 這個規則的一個明顯的例外是數組,它最好用 clone方法復制。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM