在寫一個HashSet時候有個需求,是判斷HashSet中是否已經存在對象,存在則取出,不存在則add添加。HashSet也是經過HashMap實現,只用了HashMap的key,value都存儲一個贅餘的Object,以下是HashSet中持有的HashMap對象,add函數:數組
private transient HashMap<E,Object> map; // Dummy value to associate with an Object in the backing Map private static final Object PRESENT = new Object();
public boolean add(E e) { return map.put(e, PRESENT)==null; }
中在HashMap中的hash函數判斷key是否存在,以下圖所示:網絡
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
在看到這段代碼時疑問產生了,爲何hash函數這麼設計?查過資料以後解釋以下(以下內容來自網絡-知乎胖胖的答案):函數
這段代碼叫「擾動函數」。spa
你們都知道上面代碼裏的key.hashCode()函數調用的是key鍵值類型自帶的哈希函數,返回int型散列值。
理論上散列值是一個int型,若是直接拿散列值做爲下標訪問HashMap主數組的話,考慮到2進制32位帶符號的int表值範圍從-2147483648到2147483648。先後加起來大概40億的映射空間。只要哈希函數映射得比較均勻鬆散,通常應用是很難出現碰撞的。
但問題是一個40億長度的數組,內存是放不下的。你想,HashMap擴容以前的數組初始大小才16。因此這個散列值是不能直接拿來用的。用以前還要先作對數組的長度取模運算,獲得的餘數才能用來訪問數組下標。源碼中模運算是在這個indexFor( )函數裏完成的。設計
bucketIndex = indexFor(hash, table.length);
indexFor的代碼也很簡單,就是把散列值和數組長度作一個"與"操做,code
static int indexFor(int h, int length) { return h & (length-1); }
順便說一下,這也正好解釋了爲何HashMap的數組長度要取2的整數冪。由於這樣(數組長度-1)正好至關於一個「低位掩碼」。「與」操做的結果就是散列值的高位所有歸零,只保留低位值,用來作數組下標訪問。以初始長度16爲例,16-1=15。2進製表示是00000000 00000000 00001111。和某散列值作「與」操做以下,結果就是截取了最低的四位值。對象
10100101 11000100 00100101 & 00000000 00000000 00001111 ---------------------------------- 00000000 00000000 00000101 //高位所有歸零,只保留末四位
但這時候問題就來了,這樣就算個人散列值分佈再鬆散,要是隻取最後幾位的話,碰撞也會很嚴重。更要命的是若是散列自己作得很差,分佈上成等差數列的漏洞,剛好使最後幾個低位呈現規律性重複,就無比蛋疼。blog
時候「擾動函數」的價值就體現出來了,說到這裏你們應該猜出來了。看下面這個圖,內存
右位移16位,正好是32bit的一半,本身的高半區和低半區作異或,就是爲了混合原始哈希碼的高位和低位,以此來加大低位的隨機性。並且混合後的低位摻雜了高位的部分特徵,這樣高位的信息也被變相保留下來。
最後咱們來看一下Peter Lawley的一篇專欄文章《An introduction to optimising a hashing strategy》裏的的一個實驗:他隨機選取了352個字符串,在他們散列值徹底沒有衝突的前提下,對它們作低位掩碼,取數組下標。ci
結果顯示,當HashMap數組長度爲512的時候,也就是用掩碼取低9位的時候,在沒有擾動函數的狀況下,發生了103次碰撞,接近30%。而在使用了擾動函數以後只有92次碰撞。碰撞減小了將近10%。看來擾動函數確實仍是有功效的。但明顯Java 8以爲擾動作一次就夠了,作4次的話,多了可能邊際效用也不大,所謂爲了效率考慮就改爲一次了。