Hashtables提供了一個很有用的方法可以讓應用程式的效能達到最佳。
Hashtables(雜湊表)在電腦領域中已不是新概念了。它們是用來加快電腦的處理速度的,用當今的標準來處理,速度非常慢,而它們可以讓你在查詢許多資料條目時,很快地找到一個特殊的條目。 儘管現代的機器速度已快了幾千倍,但是為了得到應用程式的最佳效能,hashtables仍然是個很有用的方法。
設想一下,你有一個包含約一千筆記錄的資料檔案??例如一個小企業的客戶記錄還有一個程序,它把記錄讀到內存中進行處理。每個記錄包含一個唯一的五位數的客戶ID號碼、客戶名字、地址、帳戶結餘等等。假設記錄不是按客戶ID號順序分類的,所以,如果程式要將客戶號碼作為「key」 來尋找一個特殊的客戶記錄,唯一的查找方法就是連續地搜尋每個記錄。有時侯,它會很快找到你需要的記錄;但有時侯,在程序找到你需要的記錄之前,它幾乎已搜索到了最後一條記錄。如果要在1,000筆記錄中搜索,那麼查找任何一筆記錄都需要程式平均核對500.5 ((1000 + 1 )/2)筆記錄。如果你經常需要查找數據,你應該需要一個更快的方法來找到一筆記錄。
一種加快搜尋的方法就是把記錄分成幾段,這樣,你就不用搜尋一個很大的清單了,而是搜尋幾個短的清單。對於我們數字式的客戶ID號,你可以建10個列表??以0開頭的ID號組成一個列表,以1開頭的ID號組成一個列表,依此類推。那麼要找客戶ID號碼38016,你只要搜尋以3開頭的清單就行了。 如果有1,000筆記錄,每個清單的平均長度為100 (1,000筆記錄被分成10個清單),那麼搜尋一筆記錄的平均比較次數就降到了約50(見圖1)。
當然,如果約十分之一的客戶號碼是以0開頭的,另外十分之一是以1開頭的,等等,那麼這種方法會很適合。如果90%的客戶號碼以0開頭,那麼那個清單就會有900筆記錄,每次查找平均需要進行450次比較。另外,程式需要執行的搜尋有90%都是針對以0開頭的號碼的。因此,平均比較數就大大超過簡單數學運算的範圍了。
如果我們可以以這樣一種方式在我們的清單中分配記錄,情況就會好一些,即每個清單約有相同條目的記錄,而不管鍵值中數字的分佈。我們需要一種方法能夠把客戶號碼混合在一起並更好地分佈結果。例如,我們可以取號碼中的每位數,乘以某個大的數(隨著數字位置的不同而不同),然後將結果相加產生一個總數, 把這個數除以10,並將餘數作為索引值(index)。當讀入記錄時,程式會在客戶號碼上執行這個雜湊(hash) 函數來確定記錄屬於哪個清單。當使用者需要查詢時,將同一個雜湊函數作為一個「key」用於客戶號碼,這樣就可以搜尋正確的清單了。像這樣的一個資料結構就稱為一個雜湊表(hashtable)。
Java中的Hashtables
Java包含兩個類, java .util.Hashtable 和java .util.HashMap,它們提供了一個多種用途的hashtable機制。這兩個類別很相似,通常提供相同的公有介面。但它們的確有一些重要的不同點,我在後面會講到。
Hashtable和HashMap物件可以讓你把一個key和一個value結合起來,並用put () 方法把這對key/value輸入到表中。然後你可以透過呼叫get()方法,把key當作參數來得到這個value(值)。只要滿足兩個基本的要求, key和value可以是任何物件。注意,因為key和value必須是對象,所以原始型別(primitive types)必須運用諸如Integer(int)的方法轉換成物件。
為了將一個特定類別的物件用做一個key,這個類別必須提供兩個方法,equals() 和hashCode()。這兩個方法在java .lang.Object中,所以所有的類別都可以繼承這兩個方法;但是,這兩個方法在Object類別中的實作一般沒什麼用,所以你通常需要自己重載這兩個方法。
Equals ()方法把它的物件同另一個物件進行比較,如果這兩個物件代表相同的訊息,則傳回true。該方法也查看並確保這兩個物件屬於相同的類別。如果兩個參照對像是完全一樣的對象,Object.equals()會傳回true,這就說明了為什麼這個方法通常不是很適合的原因。在大多數情況下,你需要一個方法來一個欄位一個欄位地進行比較,所以我們認為代表相同資料的不同物件是相等的。
HashCode()方法透過運用物件的內容執行一個雜湊函數來產生一個int值。 Hashtable和HashMap用這個值來算出一對key/value位於哪個bucket(雜湊元)(或列表)中。
作為例子,我們可以查看String 類,因為它有自己的方法來實作這兩個方法。 String.equals()對兩個String物件一個字元一個字元地進行比較,如果字串是相同的,則傳回true:
複製代碼代碼如下:
String myName = "Einstein";
// The following test is
// always true
if ( myName.equals("Einstein") )
{ ...
String.hashCode ()在一個字串上運行雜湊函數。字串中每個字元的數字代碼都乘以31,結果取決於字串中字元的位置。然後將這些計算的結果相加,得到一個總數。這個過程似乎很複雜,但是它確保能夠更好地分佈值。它也證明了你在開發你自己的hashCode()方法時,能夠走多遠,確信結果是唯一的。
例如,假設我要用一個hashtable來實作一個書的目錄,把書的ISBN號碼當作搜尋鍵來搜尋。我可以用String類別來承載細節,並準備好了equals()和hashCode()方法(請參閱列表1)。我們可以用put ()方法加入成對的key/value到hashtable中(見列表2)。
Put ()方法接受兩個參數,它們都屬於Object型別。第一個參數是key;第二個參數是value。 Put ()方法呼叫key的hashCode()方法,用表中的列表數來除這個結果。把餘數當作索引值來決定該筆記錄加到哪個清單中。請注意,key在表中是唯一的;如果你用一個已經存在的key來呼叫put (),匹配的條目就被修改了,因此它參照的是一個新的值,而舊的值被回傳了(當key在表中不存在時, put ()會傳回空值)。
要讀取表中的一個值,我們把搜尋鍵用於get()方法。它傳回一個轉換到正確類型的Object參照:
複製代碼代碼如下:
BookRecord br =
(BookRecord)isbnTable.get(
"0-345-40946-9");
System.out.println(
"Author: " + br.author
+ " Title: " + br.title);
另一個有用的方法是remove(),其用法同get()幾乎一樣,它把條目從表中刪除,並傳回給呼叫程式。
你自己的類
如果你想把一個原始型別用做一個key,你必須建立一個同等類型的物件。例如,如果你想用一個整數key,你應該用建構器Integer(int)從整數中產生一個物件。所有的封裝類別??如Integer、Float和Boolean都把原始值看做是對象,它們重載了equals()和hashCode() 方法,因此,它們可以被用做key。 JDK中提供的許多其它的類別也是這樣的(甚至Hashtable和HashMap類別都實作它們自己的equals() 和hashCode()方法),但你把任何類別的物件都用做hashtable keys前,應該查看檔案。看看類別的來源,看看equals()和hashCode()是如何實現的,也很有必要。例如,Byte、Character、 Short和Integer都會傳回所代表的整數值作為雜湊碼。這可能適合,也可能不適合你的需求。
在Java中運用Hashtables
如果你想創建一個hashtable,這個hashtable運用你自己定義的一個類別的物件作為key,那麼你應該確信這個類別的equals()和hashCode()方法提供有用的值。首先查看你擴展的類,確定它的實作是否滿足你的需求。如果沒有,你應該重載方法。
任何equals()方法的基本設計限制是,如果傳遞給它的物件屬於同一個類,而且它的資料欄位設定為表示同樣資料的值,那麼它就應該傳回true。你也應該確信,如果傳遞一個空的參數給該方法,那麼你的程式碼返回
複製代碼代碼如下:
false:public boolean equals(Object o)
{
if ( (o == null)
|| !(o instanceof myClass))
{
return false;
}
// Now compare data fields...
另外,在設計一個hashCode()方法時,應該記住一些規則。首先,該方法必須為一個特定的物件傳回相同的值,而不管這個方法被呼叫了多少次(當然,只要物件的內容在呼叫之間沒有改變,在將一個物件用做一個hashtable的key時,應該避免這一點)。第二,如果由你的equals()方法定義的兩個物件是相等的,那麼它們也必須產生相同的雜湊碼。第三,這更像是一個方針,而不是一個原則,你應該設法設計方法,使它為不同的物件內容產生不同的結果。如果偶爾不同的物件正好產生了相同的哈希碼,這也不要緊。但是,如果該方法只能傳回範圍在1到10的值,那麼只能用10個列表,而不管在hashtable中有多少個列表。
在設計equals()和hashCode()時,另一個要記住的因素是效能問題。每次呼叫put () 或get(),都包含呼叫hashCode()來尋找正確的列表,當get()掃描列表來尋找key時,它為列表中的每個元素呼叫equals()。實作這些方法使它們盡可能快而有效地運行,尤其當你打算使你的類別公開可用時,因為其它的用戶可能想在執行速度很重要的情況下,在高效能的應用程式中運用你的類。
Hashtable性能
影響hashtable功效的主要因素是表中列表的平均長度,因為平均搜尋時間與這個平均長度直接相關。很顯然, 要減小平均長度,你必須增加hashtable中列表的數量;如果列表數量非常大,以至於大多數列表或所有列表只包含一條記錄,你就會獲得最佳的搜尋效率。然而,這樣做可能太過分了。如果你的hashtable的列表數遠遠多於資料條目,那你就沒有必要做這樣的記憶體花費了,而在某些情況下,人們也不可能接受這樣的做法。
在我們前面的例子中,我們預先知道我們有多少筆記錄1,000。知道這點後,我們就可以決定我們的hashtable 應該包含多少個列表,以便達成搜尋速度和記憶體使用效率之間最好的折中方式。然而,在許多情況下,你預先不知道你要處理多少筆記錄;資料被讀取的檔案可能會不斷擴大,或者記錄的數量可能一天一天地發生很大的變化。
隨著條目的增加,Hashtable和HashMap類別透過動態地擴展表來處理這個問題。這兩個類別都有接受表中列表最初數量的構造器,和一個作為參數的負載係數(load factor):
public Hashtable(
int initialCapacity,
float loadFactor)
public HashMap(
int initialCapacity,
float loadFactor)
將這兩個數相乘計算出一個臨界值。每次在雜湊表中新增一個新的條目時,計數就會更新,當計數超過臨界值時,表被重新設定(rehash)。 (列表數量增加到以前數量的兩倍加1,所有的條目轉移到正確的列表中。)缺省的構造器設定最初的容量為11,負載係數是0.75,所以臨界值是8。當第九筆記錄被加入表中時,就重新調整雜湊表,使其有23個列表,新的臨界值將是17(23*0.75的整數部分)。你可以看到,負載係數是哈希表中平均列表數量的上限,這意味著,在缺省情況下,哈希表很少會有許多包含不止一筆記錄的列表。比較我們最初的例子,在那個例子中,我們有1,000筆記錄,分佈在10個清單中。如果我們用預設值,這個表將會擴展到含有1,500多個清單。但你可以控制這一點。如果用負載係數相乘的列表數量大於你處理的條目數,那麼表永遠不會重製,所以我們可以仿效下面的例子:
複製代碼代碼如下:
// Table will not rehash until it
// has 1,100 entries (10*110):
Hashtable myHashTable =
new Hashtable(10, 110.0F);
你可能不想這麼做,除非你沒有為空的列表節省內存,而且不介意額外的搜索時間,這可能在嵌入系統中會出現這種情況。然而,這種方法可能很有用,因為重新設定很佔用計算時間,而這種方法可以保證永遠不會發生重新設定這種情況。
請注意,雖然呼叫put ()可以使表增大(列表數量增加),但呼叫remove()不會有相反的結果。所以,如果你有一個大的表,而且從中刪除了大部分條目,結果你會有一個大的但是大部分是空的表。
Hashtable和HashMap
Hashtable和HashMap類別有三個重要的差異。第一個不同主要是歷史原因。 Hashtable是基於陳舊的Dictionary類別的,HashMap是Java 1.2引進的Map介面的實作。
也許最重要的不同是Hashtable的方法是同步的,而HashMap的方法不是。這意味著,雖然你可以不用採取任何特殊的行為就可以在一個多線程的應用程式中用一個Hashtable,但你必須同樣地為一個HashMap提供外同步。一個方便的方法就是利用Collections類別的靜態的synchronizedMap()方法,它創建一個線程安全的Map對象,並把它作為一個封裝的對象來返回。這個物件的方法可以讓你同步存取潛在的HashMap。這麼做的結果就是當你不需要同步時,你不能切斷Hashtable中的同步(例如在一個單執行緒的應用程式中),而且同步增加了很多處理費用。
第三點不同是,只有HashMap可以讓你將空值當作一個表格的條目的key或value。 HashMap中只有一筆記錄可以是空的key,但任意數量的條目可以是空的value。這就是說,如果在表中沒有發現搜尋鍵,或者如果發現了搜尋鍵,但它是一個空的值,那麼get()將傳回null。如果有必要,就用containKey()方法來區別這兩種情況。
有些資料建議,當需要同步時,用Hashtable,反之用HashMap。但是,因為在需要時,HashMap可以被同步,HashMap的功能比Hashtable的功能更多,而且它不是基於一個陳舊的類的,所以有人認為,在各種情況下,HashMap都優先於Hashtable。
關於Properties
有時侯,你可能會想用一個hashtable來映射key 的字串到value的字串。 DOS、Windows和Unix中的環境字串就有一些例子,如key的字串PATH被對應到value的字串C: /WINDOWS;C:/WINDOWS/SYSTEM。 Hashtables是表示這些的一個簡單的方法,但Java提供了另一種方法。
Java .util.Properties類別是Hashtable的子類,設計用於String keys和values。 Properties物件的用法同Hashtable的用法相像,但類別增加了兩個節省時間的方法,你應該知道。
Store()方法把一個Properties物件的內容以一種可讀的形式儲存到一個檔案中。 Load()方法剛好相反,用來讀取文件,並設定Properties物件來包含keys和values。
注意,因為Properties擴充了Hashtable,你可以用超類別的put ()方法來加入不是String物件的keys和values。這是不可取的。另外,如果你將store()用於不包含String物件的Properties對象,store()將會失敗。作為put ()和get()的替代,你應該用setProperty()和getProperty(),它們用String參數。
好了,我希望你現在可以知道如何用hashtables來加速你的處理了