深入 HashCode 方法

爲什麼HashCode對於對象是如此的重要?
一個對象的HashCode就是一個簡單的Hash算法的實現,雖然它和那些真正的複雜的
Hash算法相比還不能叫真正的算法,它如何實現它,不僅僅是程序員的編程水平問題,
而是關係到你的對象在存取是性能的非常重要的關係.有可能,不同的HashCode可能
會使你的對象存取產生,成百上千倍的性能差別.

我們先來看一下,在JAVA中兩個重要的數據結構:HashMap和Hashtable,雖然它們有很
大的區別,如繼承關係不同,對value的約束條件(是否允許null)不同,以及線程安全性
等有着特定的區別,但從實現原理上來說,它們是一致的.所以,我們只以Hashtable來
說明:

在java中,存取數據的性能,一般來說當然是首推數組,但是在數據量稍大的容器選擇中,
Hashtable將有比數組性能更高的查詢速度.具體原因看下面的內容.

Hashtable在存儲數據時,一般先將作爲key的對象的HashCode和0x7FFFFFFF做與操作,因爲一個
對象的HashCode可以爲負數,這樣操作後可以保證它爲一個正整數.然後以Hashtable的
長度取模,得到值對象在Hashtable中的索引.

index = (o.hashCode() & 0x7FFFFFFF)%hs.length;
這個值對象就會直接放在Hashtable的第index位置,對於寫入,這和數組一樣,把一個對象
放在其中的第index位置,但如果是查詢,經過同樣的算法,Hashtable可以直接通過key得到index,

從第index取得這個值對象,而數組卻要做循環比較.所以對於數據量稍大時,Hashtable的查詢比數據
具有更高的性能.

雖然不同對象有不同的hashcode,但不同的hashCode經過與長度的取餘,就很可能產生相同的index.

極端情況下會有大量的對象產生一個相同的索引.這就是關係Hashtable性能問題的最重要的問題:

Hash衝突.

常見的Hash衝突是不同key對象最終產生了相同的索引,而一種非常甚至絕對少見的Hash衝突
是,如果一組對象的個數大過了int範圍,而HashCode的長度只能在int範圍中,所以肯定要
有同一組的元素有相同的HashCode,這樣無論如何他們都會有相同的索引.當然這種極端
的情況是極少見的,可以暫不考慮,但是對於同的HashCode經過取模,則會產中相同的索引,
或者不同的對象卻具有相同的HashCode,當然具有相同的索引.

事實上一個設計各好的HashTable,一般來說會比較平均地分佈每個元素,因爲Hashtable
的長度總是比實際元素的個數按一定比例進行自增(裝填因子一般爲0.75)左右,這樣大多
數的索引位置只有一個對象,而很少的位置會有幾個元素.所以Hashtable中的每個位置存
放的是一個鏈表,對於只有一個對象是位置,鏈表只有一個首節點(Entry),Entry的next爲
null.然後有hashCode,key,value屬性保存了該位置的對象的HashCode,key和value(對象
本身),如果有相同索引的對象進來則會進入鏈表的下一個節點.如果同一個索引中有多個
對象,根據HashCode和key可以在該鏈表中找到一個和查詢的key相匹配的對象.

從上面我看可以看到,對於HashMap和Hashtable的存取性能有重大影響的首先是應該使該
數據結構中的元素儘量大可能具有不同的HashCode,雖然這並不能保證不同的HashCode
產生不同的index,但相同的HashCode一定產生相同的index,從而影響產生Hash衝突.

對於一個象,如果具有很多屬性,把所有屬性都參與散列,顯然是一種笨拙的設計.因爲對象
的HashCode()方法幾乎無所不在地被自動調用,如equals比較,如果太多的對象參與了散列.
那麼需要的操作常數時間將會增加很大.所以,挑選哪些屬性參與散列絕對是一個編程水平
的問題.

從實現來說,一般的HashCode方法會這樣:

return Attribute1.HashCode() + Attribute1.HashCode()..[+super.HashCode()],

我們知道,每次調用這個方法,都要重新對方法內的參與散列的對象重新計算一次它們的
HashCode的運算,如果一個對象的屬性沒有改變,仍然要每次都進行計算,所以如果設置一
個標記來緩存當前的散列碼,只要當參與散列的對象改變時才重新計算,否則調用緩存的
hashCode,這可以從很大程度上提高性能.


默認的實現是將對象內部地址轉化爲整數作爲HashCode,這當然能保證每個對象具有不同
的HasCode,因爲不同的對象內部地址肯定不同(廢話),但java語言並不能讓程序員獲取對
象內部地址,所以,讓每個對象產生不同的HashCode有着很多可研究的技術.

如果從多個屬性中採樣出能具有平均分佈的hashCode的屬性,這是一個性能和多樣性相矛
盾的地方,如果所有屬性都參與散列,當然hashCode的多樣性將大大提高,但犧牲了性能,
而如果只能少量的屬性採樣散列,極端情況會產生大量的散列衝突,如對"人"的屬性中,如
果用性別而不是姓名或出生日期,那將只有兩個或幾個可選的hashcode值,將產生一半以上
的散列衝突.所以如果可能的條件下,專門產生一個序列用來生成HashCode將是一個好的選
擇(當然產生序列的性能要比所有屬性參與散列的性能高的情況下才行,否則還不如直接用
所有屬性散列).

如何對HashCode的性能和多樣性求得一個平衡,可以參考相關算法設計的書,其實並不一定
要求非常的優秀,只要能盡最大可能減少散列值的聚集.重要的是我們應該記得HashCode對
於我們的程序性能有着生要的影響,在程序設計時應該時時加以注意.

從上面的過程我們可以看到,Object類花費了很大精力完成了一個hashCode功能,但實際上,我們

的對象的多少機會是用來做hash型數據結構的key的?可以說95%以上的時候,一個對象的hashCode

是一個浪費,因爲你根本用不到它。遺憾的是這個hashCode被設計成Object類的方法,其實它更應該

在一個接口中定義。當你需要把一個對象作爲hash型數據結構r key由這個數據結構檢查它是否實現了

hashCode,而其它對象則根本不需要考慮hashCode.

另外提醒一點,千萬不要用hashCode作爲持久化唯一性驗證。比如你把"mypasswd"的hashCode

存到數據庫作爲明碼的加密方式。當你的JDK版本發生改變時,String的hashCode完全有可能用一種

新的方式實現。而你原來的hashCode將完全不能驗證,結果你是所有用戶登錄失效。

 

發佈了83 篇原創文章 · 獲贊 6 · 訪問量 4萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章