Effective Java Item 13 - 明智地覆寫 clone() 方法
整理Effective Java書中Item 13: Override clone judiciously 心得筆記
主旨
Cloneable 介面的目的是讓類別宣告它們允許複製物件,然而它的設計有許多缺陷。本文將介紹如何正確覆寫 clone() 方法,並探討 Cloneable 介面的問題與替代方案。
劃重點
Cloneable介面的缺陷:Cloneable介面本身不包含任何方法,且Object類別的clone()方法是受保護的,因此直接調用clone()方法可能會失敗。- 複製的契約:
clone()方法應該返回物件的副本,且相同物件的副本應該擁有相同的hashCode,並且與原物件不同。 - 何時覆寫
clone()方法:覆寫clone()方法的類別必須遵循一些複雜且難以強制執行的協議,並且在某些情況下,clone()方法可能並不是最佳的選擇。 - 替代方案:使用複製建構函數或工廠方法:複製建構函數或工廠方法提供了比
clone()方法更可靠的物件複製方法。
Cloneable 介面與 clone() 方法的設計缺陷
Cloneable 介面的目的在於讓類別宣告它們支援複製物件,但 Cloneable 本身不包含任何方法。它僅影響 Object 類別的 clone() 方法的行為,若一個類別實作 Cloneable,Object 的 clone() 方法將返回該類別物件的逐字段複製;若沒有實作 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接口的設計缺陷,最好考慮使用複製建構函數或工廠方法來實現物件的複製。