HashMap原始碼之hash()函式分析(JDK 1.8)
我們知道,使用雜湊的容器,其高效能的主要影響因素之一就是hash值。
在HashMap中,為了更好的效能,我們希望作為Key的物件提供一個合理的hash函式以便能將其合理的分配到桶中。
而在實際的HashMap中,對從物件獲取的hash值又做了調整。
我們先看原始碼:
- staticfinalint hash(Object key){
- int h;
- return(key ==null)?0:(h = key.hashCode())^(h >>>16);
- }
我們可以將其詳細步驟等價改為如下:
- staticfinalint hash(Object
- if(key ==null)
- return0;
- int h = key.hashCode();
- int temp = h >>>16;
- int newHash = h ^ temp;
- return newHash;
- }
程式碼過程的直接翻譯就是:如果Key值為null,返回0;如果Key值不為空,返回原hash值和原hash值無符號右移16位的值按位異或的結果。
我們知道,按位異或就是把兩個數按二進位制,相同就取0,不同就取1。
比如:0101 ^ 1110 的結果為 1011。(記得以前上數位電路課的時候學過)異或的速度是非常快的。
把一個數右移16位即丟棄低16為,就是任何小於216
任何一個數,與0按位異或的結果都是這個數本身(很好驗證)。
所以這個hash()函式對於非null的hash值,僅在其大於等於216的時候才會重新調整其值。
但是調整後又什麼好處呢?
我們先看下put的時候,這個hash值是怎麼處理(部分原始碼)的:
- public V put(K key, V value){
- return putVal(hash(key), key, value,false,true);
- }
- final V putVal(int hash, K key, V value,boolean
- Node<K,V>[] tab;Node<K,V> p;int n, i;
- if((tab = table)==null||(n = tab.length)==0)
- n =(tab = resize()).length;
- if((p = tab[i =(n -1)& hash])==null)
- tab[i]= newNode(hash, key, value,null);
- ......
- }
在尋找桶位的時候,這個hash值為與上table的zise-1,初始為16,我們就拿16來舉例.
以為演算法是hashValue & size - 1 ,此時size-1=15的二進位制為 1 1 1 1 ,也就是任意類似16進位制0x?0(二進位制最後四位為0000)的hash值,都會被儲存到位置為0的桶位上,一個桶中的元素太多,就一定會降低其效能,但是我們來看看這樣的hash值經過上面的函式處理過後的結果:
- publicclassTestHashCodeMethod{
- publicstaticvoid main(String args[])throwsException{
- finalint max =Integer.MAX_VALUE >>>4;
- Random random =newRandom(System.currentTimeMillis());
- for(int i=0;i<20;i++){
- int hash = random.nextInt(max)<<4;
- int betterHash = hash ^(hash >>>16);
- System.out.print(toBinaryString(hash));
- System.out.println("-->"+ toBinaryString(betterHash));
- }
- }
- //將整數轉換為二進位制字串,高位補0
- finalstaticchar[] digits ={'0','1'};
- staticString toBinaryString(int i){
- char[] buf =newchar[32];
- int pos =32;
- int mask =1;
- do{
- buf[--pos]= digits[i & mask];
- i >>>=1;
- }while(pos >0);
- returnnewString(buf, pos,32);
- }
- }
檢視結果該hash函式轉換後的值:
- 00011000000100011111000101100000-->00011000000100011110100101110001
- 00110111100110001011010000100000-->00110111100110001000001110111000
- 01101110000011101111100011010000-->01101110000011101001011011011110
- 00000000000111110010101100010000-->00000000000111110010101100001111
- 00110101101001101000010010010000-->00110101101001101011000100110110
- 00101111111111001011000101010000-->00101111111111001001111010101100
- 01100101111101101110100110110000-->01100101111101101000110001000110
- 00000011101101101110000110100000-->00000011101101101110001000010110
- 00100011001101010011110010110000-->00100011001101010001111110000101
- 01101101111010000001111011110000-->01101101111010000111001100011000
- 01111001111100110101000101010000-->01111001111100110010100010100011
- 00111110101001111101100110100000-->00111110101001111110011100000111
- 01011001000001101010011001110000-->01011001000001101111111101110110
- 01101000101100101110101100100000-->01101000101100101000001110010010
- 01100110111011001111110001000000-->01100110111011001001101010101100
- 00100001100011000010110001100000-->00100001100011000000110111101100
- 01100010001010000110101111110000-->01100010001010000000100111011000
- 00000011001111101111111110110000-->00000011001111101111110010001110
- 00111110100100101011111110110000-->00111110100100101000000100100010
- 01000100000101011111111110000000-->01000100000101011011101110010101
是不是發現情況都發生了好轉,原來一大批會被放到“0”桶位的hash值,現在幾乎都被更佳合理的分配到了其他桶位。
我們知道hashMap中的桶位都是以oldCap<<1(即原容量*2)來增長的,所以最終這個hash值要存放的時候,都是跟一連串二進位制的“1"作與運算的,而容量定義為int型別,java中int型別為4位元組,即32位,但是Integer.MAX為0x7fffffff,也就是231 - 1 那麼大(因為最高位被用作符號位),而取16算是一種折衷的辦法。而另一個原因,也許是跟物件本身的hash值(當然也為int)有關。
那麼這個方法就介紹這麼多了,近期準備將HashMap整個原始碼解讀一下,並分享出來,並在最終整體介紹一下Java的容器體系。
之前已發過兩篇容器的原始碼解讀,這裡給出連結: