整理Effective Java書中Item 13: Override clone judiciously 心得筆記

主旨

Cloneable 介面的目的是讓類別宣告它們允許複製物件,然而它的設計有許多缺陷。本文將介紹如何正確覆寫 clone() 方法,並探討 Cloneable 介面的問題與替代方案。

劃重點

  1. Cloneable 介面的缺陷Cloneable 介面本身不包含任何方法,且 Object 類別的 clone() 方法是受保護的,因此直接調用 clone() 方法可能會失敗。
  2. 複製的契約clone() 方法應該返回物件的副本,且相同物件的副本應該擁有相同的 hashCode,並且與原物件不同。
  3. 何時覆寫 clone() 方法:覆寫 clone() 方法的類別必須遵循一些複雜且難以強制執行的協議,並且在某些情況下,clone() 方法可能並不是最佳的選擇。
  4. 替代方案:使用複製建構函數或工廠方法:複製建構函數或工廠方法提供了比 clone() 方法更可靠的物件複製方法。

Cloneable 介面與 clone() 方法的設計缺陷

Cloneable 介面的目的在於讓類別宣告它們支援複製物件,但 Cloneable 本身不包含任何方法。它僅影響 Object 類別的 clone() 方法的行為,若一個類別實作 CloneableObjectclone() 方法將返回該類別物件的逐字段複製;若沒有實作 Cloneable,則會拋出 CloneNotSupportedException

然而,這樣的設計有一個問題:clone() 方法是受保護的,這意味著只有在類別內部或透過反射才能呼叫它。因此,即使物件實作了 Cloneable,你仍然無法直接使用 clone() 方法。

複製方法的契約

clone() 方法的契約包含以下幾點:

  • x.clone() != x:這意味著複製物件不應該等於原物件。
  • x.clone().getClass() == x.getClass():複製物件應該與原物件屬於同一個類別。
  • x.clone().equals(x):理想情況下,複製物件與原物件應該相等。

但是,這些契約並不是絕對的。例如,如果一個類別覆寫了 equals() 方法,則兩個邏輯相等的物件不一定會有相同的哈希碼。

如何實作 clone() 方法

若類別需要正確實現複製方法,並且其父類已經提供了合適的 clone() 方法,應該先呼叫 super.clone(),以獲得物件的副本。之後,若有必要,可對物件的字段進行額外處理。

以下是 PhoneNumber 類別的 clone() 方法範例:

@Override
public PhoneNumber clone() {
    try {
        return (PhoneNumber) super.clone();
    } catch (CloneNotSupportedException e) {
        throw new AssertionError(); // Can't happen
    }
}

複製可變狀態的物件

如果物件包含可變的狀態(例如,Stack 類別中的 elements 陣列),簡單的 clone() 實作可能會導致問題。因為 super.clone() 會複製物件的基本結構,但可變的字段會共享同一個參考,這會導致在修改原物件時,複製物件的狀態也會被改變。

這時需要對可變的字段進行深度複製,例如:

@Override
public Stack clone() {
    try {
        Stack result = (Stack) super.clone();
        result.elements = elements.clone(); // 深度複製
        return result;
    } catch (CloneNotSupportedException e) {
        throw new AssertionError();
    }
}

複製建構函數和工廠方法的替代方案

相比 clone() 方法,複製建構函數或工廠方法是一個更好的選擇。它們不依賴於 Cloneable 接口,並且可以更靈活地處理物件複製。

例如,PhoneNumber 類別的複製建構函數如下:

// 複製建構函數
public PhoneNumber(PhoneNumber other) {
    this.areaCode = other.areaCode;
    this.prefix = other.prefix;
    this.lineNum = other.lineNum;
}

另一種方式是使用複製工廠方法:

public static PhoneNumber of(int areaCode, int prefix, int lineNum) {
    return new PhoneNumber(areaCode, prefix, lineNum);
}

這樣的做法能避免 Cloneable 接口的複雜性和風險,並提供一個更安全的物件複製方法。

小結

  • 當覆寫 clone() 方法時,應遵循其契約,確保複製物件與原物件之間的正確關係。
  • 對於可變狀態的物件,必須實現深度複製,避免物件間的狀態共享。
  • 由於 Cloneable 接口的設計缺陷,最好考慮使用複製建構函數或工廠方法來實現物件的複製。