第3項:用私有構造器或者列舉型別強化Singleton屬性
Singleton指僅僅被例項化一次的類 [Gamma95]。Singleton通常代表無狀態的物件,例如函式(第24項)或者本質上唯一的系統元件。使類稱為Singleton會使它的客戶端測試變得十分困難,因為除非它實現了作為其型別的介面,否則不可能將模擬實現替換為單例。
實現單例的方法有兩種。 兩者都基於保持建構函式私有並匯出公共靜態成員以提供對唯一例項的訪問。 在一種方法中,該成員是final欄位:
// Singleton with public final field public class Elvis { public static final Elvis INSTANCE = new Elvis(); private Elvis() { ... } public void leaveTheBuilding() { ... } }
私有構造器只調用一次,用來初始化靜態變數 Elvis.INSTANCE
。由於缺少 public
或者 protect
屬性的構造器,這就保證了 Elvis
的全域性一致性:一旦 Evlis
類被例項化,只會存在一個 Elvis
例項,不多也不少。客戶端所做的任何事情都無法改變這一點,但有一點需要注意:享有特權的客戶端可以藉助 AccessibleObject.setAccessible
方法反射性地呼叫私有建構函式(第65項)。 如果你需要防禦此攻擊,請修改建構函式以使其在要求建立第二個例項時丟擲異常。
在實現Singleton的第二種方法中,公有的成員是個靜態工廠方法:
// Singleton with static factory public class Elvis { private static final Elvis INSTANCE = new Elvis(); private Elvis() { ... } public static Elvis getInstance() { return INSTANCE; } public void leaveTheBuilding() { ... } }
對於靜態方法 Elvis.getInstance
的所有呼叫,都會返回同一個物件引用,所以,永遠不會建立其他的 Elvis
例項(上述提醒依然適用)。
公有域方法的主要好處在於,組成類的成員的宣告很清楚地聲明瞭這個類是一個Singleton:公有的靜態域是final的,所以該域總是包含同一個物件的引用。第二個好處就是它更加簡單。
工廠方法的優勢之一在於,它提供了靈活性:在不改變其API的前提下,我們可以改變類是否應該為Singleton的想法。工廠方法返回唯一例項,但是,它可以很容易被修改,比如改成每個呼叫該方法的執行緒返回一個唯一的例項。第二個優點是,如果你的應用需要,你可以編寫泛型單例工廠(第30項)。使用靜態工廠的最後一個優點是方法參考可以用作供應商,例如 Elvis::instance
是供應商<Elvis>。除非這些優勢中的一個是相關的,否則公共領域方法更可取。(A final advantage of using a static factory is that a method reference can be used as a supplier, for example Elvis::instance is a Supplier<Elvis> .Unless one of these advantages is relevant, the public field approach is preferable.)
為了使利用這其中一種方法實現的Singleton類程式設計可序列化的(第12章),僅僅在宣告中加上“implements Serializable”是不夠的。為了維護並保證Singleton,必須宣告所有例項域都是瞬時(transient)的,並提供一個 readResolve
方法(第89項)。否則,每次反序列化時,都會建立一個新的例項,在我們的示例中,會導致“假冒的Elvis”。為了防止這種情況,要在Elvis類中加入下面這個readResolve方法:
// readResolve method to preserve singleton property private Object readResolve() { // Return the one true Elvis and let the garbage collector // take care of the Elvis impersonator. return INSTANCE; }
實現Singleton還有第三種方法。只需要編寫一個包含單個元素的列舉型別:
// Enum singleton - the preferred approach public enum Elvis { INSTANCE; public void leaveTheBuilding() { ... } }
這種方法類似於公共領域方法,但它更簡潔,免費提供序列化機制,並提供了對多個例項化的鐵定保證,即使面對複雜的序列化或反射攻擊。這種方法可能會有點不自然,但單元素列舉型別通常是實現單例的最佳方法。請注意,如果你的單例必須擴充套件 Enum
以外的超類,則不能使用此方法(儘管你可以宣告列舉來實現介面)。