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()
方法範例:
複製可變狀態的物件
如果物件包含可變的狀態(例如,Stack
類別中的 elements
陣列),簡單的 clone()
實作可能會導致問題。因為 super.clone()
會複製物件的基本結構,但可變的字段會共享同一個參考,這會導致在修改原物件時,複製物件的狀態也會被改變。
這時需要對可變的字段進行深度複製,例如:
複製建構函數和工廠方法的替代方案
相比 clone()
方法,複製建構函數或工廠方法是一個更好的選擇。它們不依賴於 Cloneable
接口,並且可以更靈活地處理物件複製。
例如,PhoneNumber
類別的複製建構函數如下:
另一種方式是使用複製工廠方法:
這樣的做法能避免 Cloneable
接口的複雜性和風險,並提供一個更安全的物件複製方法。
小結
- 當覆寫
clone()
方法時,應遵循其契約,確保複製物件與原物件之間的正確關係。 - 對於可變狀態的物件,必須實現深度複製,避免物件間的狀態共享。
- 由於
Cloneable
接口的設計缺陷,最好考慮使用複製建構函數或工廠方法來實現物件的複製。