一個實用的例子:屬性管理器
什麼是屬性文件
這裡給出一個讀取屬性(properties) 文件的單例類,作為單例模式的一個實用的例子。屬性文件如同老式的視窗編程時的.ini 文件,用於存放系統的配置信息。配置信息在屬性文件中以屬性的方式存放,一個屬性就是兩個字符串組成的對子,其中一個字符串是鍵(key),另一個字符串是這個鍵的值(value)。
大多數的系統都有一些配置常量,這些常量如果是存儲在程序內部的,那麼每一次修改這些常量都需要重新編譯程序。將這些常量放在配置文件中,系統通過訪問這個配置文件取得配置常量,就可以通過修改配置文件而無需修改程序而達到更改系統配置的目的。系統也可以在配置文件中存儲一些工作環境信息,這樣在系統重啟時,這些工作信息可以延續到下一個運行周期中。
假定需要讀取的屬性文件就在當前目錄中,且文件名為singleton.properties 。這個文件中有如下的一些屬性項。
代碼清單5:屬性文件內容
node1.item1=How
node1.item2=are
node2.item1=you
node2.item2=doing
node3.item1=?
例如,node1.item1 就是一個鍵,而How 就是這個鍵所對應的值。
Java 屬性類
Java 提供了一個工具類,稱做屬性類,可以用來完成Java 屬性和屬性文件的操作。這個屬性類的繼承關系可以從下面的類圖中看清楚。
屬性類提供了讀取屬性和設置屬性的各種方法。其中讀取屬性的方法有:
.. contains(Object value) 、containsKey(Object key): 如果給定的參數或屬性關鍵字在屬性表中有定義,該方法返回True ,否則返回False。
.. getProperty(String key)、getProperty(String key, String default) :根據給定的屬性關鍵字獲取關鍵字值。
.. list(PrintStream s) 、list(PrintWriter w) :在輸出流中輸出屬性表內容。
.. size():返回當前屬性表中定義的屬性關鍵字個數。
設置屬性的方法有:
.. put(Object key, Object value) :向屬性表中追加屬性關鍵字和關鍵字的值。
.. remove(Object key):從屬性表中刪除關鍵字。
從屬性文件加載屬性的方法為load(InputStream inStream),可以從一個輸入流中讀入一個屬性列,如果這個流是來自一個文件的話,這個方法就從文件中讀入屬性。
將屬性存入屬性文件的方法有幾個,重要的一個是store(OutputStream out, String header) ,將當前的屬性列寫入一個輸出流,如果這個輸出流是導向一個文件的,那麼這個方法就將屬性流存入文件。
為什麼需要使用單例模式
屬性是系統的一種"資源",應當避免有多余一個的對象讀取特別是存儲屬性。此外,屬性的讀取可能會在很多地方發生,創建屬性對象的地方應當在哪裡不是很清楚。換言之,屬性管理器應當自己創建自己的實例,並且自己向系統全程提供這一事例。因此,屬性文件管理器應當是一個單例模式負責。
系統設計
系統的核心是一個屬性管理器,也就是一個叫做ConfigManager 的類,這個類應當是一個單例類。因此,這個類應當有一個靜態工廠方法,不妨叫做getInstance(),用於提供自己的實例。
為簡單起見,本文在這裡采取"餓漢"方式實現ConfigManager 。例子的類圖如下所示。
本例子的源代碼如下所示。
代碼清單6:ConfigManager 的源代碼
import java.util.Properties;
import java.io.FileInputStream;
import java.io.File;
public class ConfigManager
{
/**
* 屬性文件全名
*/
private static final String PFILE =
System.getProperty("user.dir")
+ File.Separator + "Singleton.properties";
/**
* 對應於屬性文件的文件對象變量
*/
private File m_file = null;
/**
* 屬性文件的最後修改日期
*/
private long m_lastModifiedTime = 0;
/**
* 屬性文件所對應的屬性對象變量
*/
private Properties m_props = null;
/**
* 本類可能存在的惟一的一個實例
*/
private static ConfigManager m_instance =
·234·Java 與模式
new ConfigManager();
/**
* 私有的構造子,用以保證外界無法直接實例化
*/
private ConfigManager()
{
m_file = new File(PFILE);
m_lastModifiedTime = m_file.lastModified();
if(m_lastModifiedTime == 0)
{
System.err.println(PFILE +
" file does not exist!");
}
m_props = new Properties();
try
{
m_props.load(new FileInputStream(PFILE));
}
catch(Exception e)
{
e.printStackTrace();
}
}
/**
* 靜態工廠方法
* @return 返還ConfigManager 類的單一實例
*/
synchronized public static ConfigManager
getInstance()
{
return m_instance;
}
/**
* 讀取一特定的屬性項
*
* @param name 屬性項的項名
* @param defaultVal 屬性項的默認值
* @return 屬性項的值(如此項存在),默認值(如此項不存在)
*/
final public Object getConfigItem(
String name,Object defaultVal)
{
long newTime = m_file.lastModified();
// 檢查屬性文件是否被其他程序
// (多數情況是程序員手動)修改過
// 如果是,重新讀取此文件
if(newTime == 0)
{
// 屬性文件不存在
if(m_lastModifiedTime == 0)
{
System.err.println(PFILE
+ " file does not exist!");
}
else
{
System.err.println(PFILE
+ " file was deleted!!");
}
return defaultVal;
}
else if(newTime > m_lastModifiedTime)
{
// Get rid of the old properties
m_props.clear();
try
{
m_props.load(new FileInputStream(PFILE));
}
catch(Exception e)
{
e.printStackTrace();
}
}
m_lastModifiedTime = newTime;
Object val = m_props.getProperty(name);
if( val == null )
{
return defaultVal;
}
else
{
return val;
}
}
}
在上面直接使用了一個局域的常量儲存儲屬性文件的路徑。在實際的系統中,讀者可以采取更靈活的方式將屬性文件的路徑傳入。
讀者可以看到,這個管理器類有一個很有意思的功能,即在每一次調用時,檢查屬性文件是否已經被更新過。如果確實已經被更新過的話,管理器會自動重新加載屬性文件,從而保證管理器的內容與屬性文件的內容總是一致的。
怎樣調用屬性管理器
下面的源代碼演示了怎樣調用ConfigManager 來讀取屬性文件。
代碼清單7:怎樣調用ConfigManager 類以讀取屬性文件
BufferedReader reader = new BufferedReader(
new InputStreamReader(System.in));
System.out.println("Type quit to quit");
do
{
System.out.print("Property item to read: ");
String line = reader.readLine();
if(line.equals("quit"))
{
break;
}
System.out.println(ConfigManager.getInstance()
.getConfigItem(line,"Not found."));
} while(true);
上面代碼運行時的情況如下圖所示。
感興趣的讀者可以參考閱讀本書的"專題:XMLProperties 與適配器模式"一章,那裡對使用Java 屬性類和XML 文件格式做了有用的討論。
Java 語言中的單例模式
Java 語言中就有很多的單例模式的應用實例,這裡討論比較有名的幾個。
Java 的Runtime 對象
在Java 語言內部,java.lang.Runtime 對象就是一個使用單例模式的例子。在每一個Java 應用程序裡面,都有惟一的一個Runtime 對象。通過這個Runtime 對象,應用程序可以與其運行環境發生相互作用。
Runtime 類提供一個靜態工廠方法getRuntime()::
public static Runtime getRuntime();
通過調用此方法,可以獲得Runtime 類惟一的一個實例:
Runtime rt = Runtime getRuntime();
Runtime 對象通常的用途包括:執行外部命令;返回現有內存即全部內存;運行垃圾收集器;加載動態庫等。下面的例子演示了怎樣使用Runtime 對象運行一個外部程序。
代碼清單8:怎樣使用Runtime 對象運行一個外部命令
import java.io.*;
public class CmdTest
{
public static void main(String[] args) throws IOException
{
Process proc = Runtime.getRuntime().exec("notepad.exe");
}
}
上面的程序在運行時會打開notepad 程序。應當指出的是,在Windows 2000 的環境中,如果需要打開一個Word 文件,而又不想指明Word 軟件安裝的位置時,可以使用下面的做法:
Process proc = Runtime.getRuntime().exec(
"cmd /E:ON /c start MyDocument.doc");
在上面,被執行的命令是start MyDocument.doc ,開關E:ON 指定DOS 命令處理器允許命令擴展,而開關/C 指明後面跟隨的字符串是命令,並在執行命令後關閉DOS 窗口,start 命令會開啟一個單獨的窗口執行所提供的命令。
Introspector 類
一般的應用程序可能永遠也不會直接用到Introspector 類,但讀者應該知道Introspector 是做什麼的。Sun 提供了一個叫做BeanBox 的系統,允許動態地加載JavaBean ,並動態地修改其性質。BeanBox 在運行時的情況如下圖所示。
在上面的圖中顯示了BeanBox 最重要的兩個視窗,一個叫做BeanBox 視窗,另一個叫做性質視窗。在上面的BeanBox 視窗中顯示了一個Juggler Bean 被放置到視窗中的情況。相應的,在性質視窗中顯示了Juggler Bean 的所有性質。所有的Java 集成環境都提供這種功能,這樣的系統就叫做BeanBox 系統。
BeanBox 系統使用一種自省(Introspection )過程來確定一個Bean 所輸出的性質、事件和方法。這個自省機制是通過自省者類,也即java.util.Introspector 類實現的;這個機制是建立在Java 反射(Reflection) 機制和命名規范的基礎之上的。比如,Introspector 類可以確定Juggler Bean 所支持的所有的性質,這是因為Introspector 類可以得到所有的方法,然後將其中的取值和賦值方法以及它們的特征加以比較,從而得出結果。顯然,在整個BeanBox 系統中只需要一個Introspector 對象,下面所示就是這個類的結構圖。
可以看出,Introspector 類的構造子是私有的,一個靜態工廠方法instantiate() 提供了Instrospector 類的惟一實例。換言之,這個類是單例模式的應用。
java.awt.Toolkit 類
Toolkit 類是一個非常有趣的單例模式的例子。Toolkit 使用單例模式創建所謂的Toolkit 的默認對象,並且確保這個默認實例在整個系統中是惟一的。Toolkit 類提供了一個靜態的方法getDefaultToolkit() 來提供這個惟一的實例,這個方法相當於懶漢式的單例方法,因此整個方法都是同步化的。
代碼清單9:getDefaultToolkit() 方法
public static synchronized Toolkit
getDefaultToolkit()
{
......
}
Toolkit 類的類圖如下所示。
其中性質defaultToolkit 實際上就是靜態的getDefaultToolkit 類。有趣的是,由於Toolkit 是一個抽象類,因此其子類如果提供一個私有的構造子,那麼其子類便是一個正常的單例類;而如果其子類作為具體實現提供一個公開的構造子,這時候這個具體子類便是" 不完全"的單例類。關於"不完全"的單例類的討論請見本章後面的"專題:不完全的單例類"一節。
模版方法模式
同時,熟悉模版方法模式的讀者可以看出,getDefaultToolkit() 方法實際上是一個模版方法。私有構造子是推遲到子類實現的剩余邏輯,根據子類對這個剩余邏輯的不同實現,子類就可以提供完全不同的行為。對Toolkit 的子類而言,私有構造子依賴於操作系統,不同的子類可以根據不同的操作系統而給出不同的邏輯,從而使Toolkit 的子類對不同的操作系統給出不同的行為。
javax.swing.TimerQueue 類
這是一個不完全的單例類,由於這個類是在Swing 的定時器類中使用的,因此我們將在後面介紹。