java HashMap 源碼分析

一、HashMap內部的數據結構是什麼?

數組+單向鏈表
image.png

二、怎麼驗證內部結構是數組和單向鏈表?

a、數組:通過HashMap源碼知道、HashMap內部有個屬性 transient Node<K,V>[] table
b、單向鏈表:內部類Node裏面維護了一個next的屬性 Node<K,V> next,是指向下一個節點的;

三、HashMap裏面爲什麼會有hash的存在?hash計算的理解?

我們先看下我的事例代碼

public class HashMapDemo {

    public static void main(String[] args) {
        HashMap<String,String> map=new HashMap<String, String>();
        String keys="names";
        String values="zhangsans";
        map.put(keys,values);
        System.out.println("數組的默認大小:"+ (1<<4));
        System.out.println("對應二進制:"+binaryToDecimal((15)));
        int hashCodes=keys.hashCode();
        System.out.println("hashCode值:"+hashCodes);
        System.out.println("對應二進制:"+binaryToDecimal(hashCodes));
        int dw=(hashCodes >>> 16);
        System.out.println("向右移16位,高位補0 值:"+dw);
        System.out.println("對應二進制:"+binaryToDecimal(dw));
        int yhValues=hashCodes ^ dw;
        System.out.println("異或運算結果:"+yhValues);
        System.out.println("對應二進制:"+binaryToDecimal(yhValues));
        //(n:數組長度 - 1) & hash :hash值
        System.out.println("下標計算:"+(yhValues & (16-1)));
    }

    /**
     * 轉換二進制
     * @param n
     * @return
     */
    public static String binaryToDecimal(int n){
      String str = "";
       for(int i = 31;i >= 0; i--){
           int ys=(n >>> i & 1);
           str = str + ys;
       }
       return str;
    }
}

結果:

數組的默認大小:16
對應二進制:00000000000000000000000000001111
hashCode值:104585032
對應二進制:00000110001110111101011101001000
向右移16位,高位補0 值:1595
對應二進制:00000000000000000000011000111011
異或運算結果:104583539
對應二進制:00000110001110111101000101110011
下標計算:3
  1. 爲了Node節點數據落在何處 ;
  2. 當put數據的時候,我們通過源碼知道,會先經過數組,而數組的默認大小是16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4 
//這個時候我們知道了數組默認大小,那麼我們put的數據放在哪塊呢?隨機放?

a. 首先我們會 通過對 Object.hashCode() 得到一個整型數,【3373707】;
b. 根據數組默認大小,我們知道數組下標必須在0-15(擴容的下面說),那麼我們的算法肯定得控制在這個值之類,否則就會發生數組越界
c. 而hashCode是通過 key.hashCode()高16位和低16位進行異或運算得到一個整數類型的值

static final int hash(Object key) {
   int h;
   return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

d. 最後經過 & “與”運算,得到下標;

(n - 1) & hash

這個地方順帶說下:
這個也正好解釋了爲啥HashMap的數組長度要取2的整次冪。數組長度-1,正好相當於一個“低位掩碼”,然後呢, & “與”運算的結果就是散列值得高位全部歸零,只保留低位值;然後我們拿數組默認長度來說,16,下標就是 0-15;然後倒除法得到二進制值 1111

//高位全部歸零,只保留末四位

	0000 0110 0011 1011 1101 0111 0100 1000
&	0000 0000 0000 0000 0000 0000 0000 1111
-------------------------------------------
	0000 0000 0000 0000 0000 0000 0000 1000    

看到這裏,我們大概就會想到一個問題,這樣就算我的散列值分佈再鬆散,要是隻取最後幾位的話,碰撞也會很嚴重。分佈上成等差數列的漏洞,恰好使最後幾個低位呈現規律性重複。

這時候 “擾動函數" 的價值就體現出來了, 下面是所有值得二進制變化

h=hashCode()	    	0000 0110 0011 1011 1101 0111 0100 1000
—————————————————————————————————————————————————————————————————
h >>> 16   		        0000 0000 0000 0000 0000 0110 0011 1011
—————————————————————————————————————————————————————————————————
hash ^ hash >>>16		0000 0110 0011 1011 1101 0001 0111 0011
—————————————————————————————————————————————————————————————————
16-1					0000 0000 0000 0000 0000 0000 0000 1111
—————————————————————————————————————————————————————————————————
(16-1) & hash           0000 0000 0000 0000 0000 0000 0000 0011
—————————————————————————————————————————————————————————————————
				                                              3

在上面大家可以比對下 hh >>> 16是不是發現了一個很有意思的東西,自己的高半區和低半區做異或,就是爲了混合原始哈希碼的高位和低位,以此來加大低位的隨機性。而且混合後的低位摻雜了高位的部分特徵,這樣高位的信息也被變相保留下來。
JDK8只做了一次干擾,爲什麼呢?推薦大家看下《An introduction to optimising a hashing strategy》的一個實驗應該就是明白了
e. 其實上面b-d可以直接替換成 Object.hashCode() % 16這樣得到的結果和&運算的結果都是保證在 0-15的,但是對於計算機來說,&運算的效率比取模運算的效率高。(這裏也是一個注意點)

四、HashMap,put的流程(裏面包含了很多面點,問題和解釋)

image.png

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章