Java的23種設計模式----單例模式
Java的23種設計模式——單例模式
1、引子
單例模式是設計模式中使用很頻繁的一種模式,在各種開源框架、應用系統中多有應用,並對有人提出的“單例模式是邪惡的”這個觀點進行了一定的分析。
2、定義與結構
- 單例模式又叫做“單態模式”或者“單件模式”
- 在GOF書中給出的定義為:
- 保證一個類僅有一個例項,並提供一個訪問它的全域性訪問點
- 單例模式中的“單例”通常用來代表那些本質上具有唯一性的系統元件(或者叫做資源)。比如檔案系統、資源管理器等等。
- 單例模式的目的就是要控制特定的類只產生一個物件,當然也允許在一定情況下靈活的改變物件的個數
- 那麼如何來實現單例模式呢?(一個類的物件的產生,是由類建構函式來完成的,如果想限制物件的產生,)
- 將建構函式變為私有的(至少是受保護的),使的外面的類不能通過引用來產生物件
- 同時為了保證類的可用性,就必須提供一個自己的物件以及訪問這個物件的靜態方法
現在對單例模式有了大概的瞭解了吧,其實單例模式在實現上是非常簡單的——只有一個角色,而客戶則通過呼叫類方法來得到類的物件
單例模式可分為有狀態的和無狀態的。有狀態的單例物件一般也是可變的單例物件,多個單態物件在一起就可以作為一個狀態倉庫一樣向外提供服務。沒有狀態的單例物件也就是不變單例物件,僅用做提供工具函式
3、程式碼實現
- 在單例模式中的實現上有幾種不同的方式。
3.1、餓漢式
- 《Java與模式》中被稱為“餓漢式”
/**
* 餓漢式
*/
public class SingleDemo {
//在自己內部定義自己一個例項
//注意這裡是private,只供內部使用
private static SingleDemo instance=new SingleDemo();
//如上面所述,將建構函式私有化
private SingleDemo(){
}
//靜態工廠方法,提供了一個供外部訪問得到物件的方法
public static SingleDemo getInstance(){
return instance;
}
}
3.2、懶漢式
/**
* 懶漢式
*/
public class SingleDemo {
//在自己內部定義自己一個例項
//注意這裡是private,只供內部使用
//和上面有什麼不同?
private static SingleDemo instance=null;
//將建構函式私有化
private SingleDemo(){
}
//靜態工廠方法
public static synchronized SingleDemo getInstance(){
//這個方法比“餓漢式”有所改進
if(instance==null){
instance= new SingleDemo();
}
return instance;
}
}
3.3、兩者對比
- “餓漢式”和“懶漢式”有什麼不同呢?
首先對比一下實現方式
- 建構函式都是私有化,無法使用建構函式來得到類的例項,但是這樣也使得類失去了多型性(大概這就是為什麼有人喜歡將這種模式稱之為‘單態模式’的原因吧!)
- 懶漢式中,對靜態方法工廠進行了同步處理,為了防止多執行緒環境中產生多個例項,餓漢式中不存在這種情況
- 餓漢式中類載入的時候例項化,這樣對此載入會造成對此例項化,懶漢式使用了同步處理,在反應速度上會比餓漢式慢一些
在 《java 與模式》書中提到,就 java 語言來說,第一種方式更符合 java 語言本身的特點。
以上兩種實現方式均失去了多型性,不允許被繼承。
3.4、附加(登錄檔方式)
還有另外一種靈活點的實現,將建構函式設定為受保護的,這樣允許被繼承產生子類。這種方式在具體實現上又有所不同,可以將父類中獲得物件的靜態方法放到子類中再實現;也可以在父類的靜態方法中進行條件判斷來決定獲得哪一個物件;在 GOF 中認為最好的一種方式是維護一張存有物件和對應名稱的登錄檔(可以使用 HashMap 來實現)。
- 下面的實現參考《java 與模式》採用帶有登錄檔的方式
import java.util.HashMap;
/**
* 《java與模式》採用帶有登錄檔的方式
*/
public class SingleDemo {
//用來存放對應關係
private static HashMap sinRegistry =new HashMap();
static private SingleDemo s =new SingleDemo();
//受保護的建構函式
protected SingleDemo(){
}
public static SingleDemo getInstance(String name){
if(name==null){
name="Singleton";
}
if(sinRegistry.get(name)==null){
try {
sinRegistry.put(name, Class.forName(name).newInstance());
} catch (Exception e) {
e.getStackTrace();
}
}
return (SingleDemo)(sinRegistry.get(name));
}
}
/**
* SingleDemoChild
*/
public class SingleDemoChild extends SingleDemo{
public SingleDemoChild(){
}
static public SingleDemoChild getInstance(){
return (SingleDemoChild)SingleDemo.getInstance("SingleDemoChild");
}
}
- 由於在 java 中子類的建構函式的範圍不能比父類的小,所以可能存在不守規則的客戶程式使用其建構函式來產生例項,造成單例模式失效。
4、單例模式邪惡論
- 單例模式在 java 中的使用存在很多陷阱和假象,這使得沒有意識到單例模式使用侷限性的你在系統中佈下了隱患……
- 下面一一列舉單例模式的陷阱
- 多個虛擬機器
當系統中的單例類被拷貝執行在多個虛擬機器下的時候,在每一個虛擬機器下都可以建立一個例項物件。在使用了EJB、JINI、RMI技術的分散式系統中,由於中介軟體遮蔽掉了分散式系統在物理上的差異,所以對於你來說,想知道具體哪個虛擬機器下執行著哪個單例是很困難的。
因此,在使用以上分散式技術系統中,應該避免使用存在狀態的單例模式,因為一個有狀態的單例類,在不同的虛擬機器上,各個單例物件儲存的狀態很可能是不一樣的,問題也就隨之產生。而且在 EJB 中不要使用單例模式來控制訪問資源,因為這是由 EJB 容器來負責的。在其它的分散式系統中,當每一個虛擬機器中的資源是不同的時候,可以考慮使用單例模式來進行管理。
- 多個類載入器
當存在多個類載入器載入類的時候,即使它們載入的是相同包名,相同類名甚至每個位元組都完全相同的類,也會被區別對待的。因為不同的類載入器會使用不同的名稱空間(namespace)來區分同一個類。因此,單例類在多載入器的環境下會產生多個單例物件。也許你認為出現多個類載入器的情況並不是很多。其實多個類載入器存在的情況並不少見。在很多 J2EE 伺服器上允許存在多個 servlet 引擎,而每個引擎是採用不同的類載入器的;瀏覽器中 applet 小程式通過網路載入類的時候,由於安全因素,採用的是特殊的類載入器,等等。這種情況下,由狀態的單例模式也會給系統帶來隱患。因此除非系統由協調機制,在一般情況下不要使用存在狀態的單例模式。
- 錯誤的同步處理
在使用上面介紹的懶漢式單例模式時,同步處理的恰當與否也是至關重要的。不然可能會達不到得到單個物件的效果,還可能引發死鎖等錯誤。因此在使用懶漢式單例模式時一定要對同步有所瞭解。不過使用餓漢式單例模式就可以避免這個問題。
- 子類破壞了物件控制
由於類建構函式變得不再私有,就有可能失去對物件的控制。這種情況只能通過良好的文件來規範
- 序列化(可序列化)
為了使一個單例類變成可序列化的,僅僅在宣告中新增“implements Serializable”是不夠的。因為一個序列化的物件在每次返序列化的時候,都會建立一個新的物件,而不僅僅是一個對原有物件的引用。為了防止這種情況,可以在單例類中加入 readResolve 方法。 關於這個方法的具體情況請參考《Effective Java》一書第 57 條建議。其實物件的序列化並不僅侷限於上述方式,還存在基於 XML 格式的物件序列化方式。這種方式也存在上述的問題,所以在使用的時候要格外小心。
5、題外話
- 拋開單例模式,使用下面一種簡單的方式也能得到單例,而且如果你確信此類永遠是單例的,使用下面這種方式也許更好一些。
public static final SingleDemo INSTANCE = new SingleDemo();
- 而使用單例模式提供的方式,這可以在不改變 API 的情況下,改變我們對單例類的具體要求。