Tips
《Effective Java, Third Edition》一書英文版已經出版,這本書的第二版想必很多人都讀過,號稱Java四大名著之一,不過第二版2009年出版,到現在已經將近8年的時間,但隨着Java 6,7,8,甚至9的發布,Java語言發生了深刻的變化。
在這里第一時間翻譯成中文版。供大家學習分享之用。
3. 使用私有構造方法或枚類實現Singleton屬性
單例是一個僅實例化一次的類[Gamma95]。單例對象通常表示無狀態對象,如函數(條目 24)或一個本質上唯一的系統組件。讓一個類成為單例會使測試它的客戶變得困難,因為除非實現一個作為它類型的接口,否則不可能用一個模擬實現替代單例。
有兩種常見的方法來實現單例。兩者都基於保持構造方法私有和導出公共靜態成員以提供對唯一實例的訪問。在第一種方法中,成員是final
修飾的屬性:
// Singleton with public final field
public class Elvis {
public static final Elvis INSTANCE = new Elvis();
private Elvis() { ... }
public void leaveTheBuilding() { ... }
}
私有構造方法只調用一次,來初始化公共靜態 final Elvis.INSTANCE
屬性。缺少一個公共的或受保護的構造方法,保證了全局的唯一性:一旦Elvis類被初始化,一個Elvis的實例就會存在——不多也不少。客戶端所做的任何事情都不能改變這一點,但需要注意的是:特權客戶端可以使用AccessibleObject.setAccessible
方法,以反射方式調用私有構造方法(條目 65)。如果需要防御此攻擊,請修改構造方法,使其在請求創建第二個實例時拋出異常。
在第二個實現單例的方法中,公共成員是一個靜態的工廠方法:
// 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實例(與前面提到的警告相同)。
公共屬性方法的主要優點是API明確表示該類是一個單例:公共靜態屬性是final的,所以它總是包含相同的對象引用。 第二個好處是它更簡單。
靜態工廠方法的一個優點是,它可以靈活地改變你的想法,無論該類是否為單例而不必更改其API。 工廠方法返回唯一的實例,但是可以修改,比如,返回調用它的每個線程的單獨實例。 第二個好處是,如果你的應用程序需要它,可以編寫一個泛型單例工廠(generic singleton factory )(條目30)。 使用靜態工廠的最后一個優點是方法引用可以用supplier
,例如Elvis :: instance
等同於Supplier<Elvis>
。 除非與這些優點相關的,否則公共屬性方法是可取的。
創建一個使用這兩種方法的單例類(第12章),僅僅將implements Serializable
添加到聲明中是不夠的。為了維護單例的保證,聲明所有的實例屬性為transient
,並提供一個readResolve
方法(條目89)。否則,每當序列化實例被反序列化時,就會創建一個新的實例,在我們的例子中,導致出現新的Elvis實例。為了防止這種情況發生,將這個readResolve
方法添加到Elvis類:
// 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;
}
實現一個單例的第三種方法是聲明單一元素的枚舉類:
// Enum singleton - the preferred approach
public enum Elvis {
INSTANCE;
public void leaveTheBuilding() { ... }
}
這種方式類似於公共屬性方法,但更簡潔,提供了免費的序列化機制,並提供了針對多個實例化的堅固保證,即使是在復雜的序列化或反射攻擊的情況下。這種方法可能感覺有點不自然,但是單一元素枚舉類通常是實現單例的最佳方式。注意,如果單例必須繼承Enum
以外的父類(盡管可以聲明一個Enum
來實現接口),那么就不能使用這種方法。