HashMap的底層實現原理基於jdk1.8

本文參考了知乎大佬清淺池塘和程序員小灰的文章,把兩位的精華搬運過來,主要是想鞏固一下自己的記憶,鏈接:https://zhuanlan.zhihu.com/p/28501879
https://zhuanlan.zhihu.com/p/31610616
當我們面試大廠的時候,通常都會問到這個問題,最近也在準備面試所以就着重看了下,平時工作學習還是要多看些源碼,多思考的。

map有這麼幾中,TreeMap,HashMap,LinkedHashMap,HashTable(已被廢棄),ConcurrentHashMap, HashMap,HashTable,ConcurrentHashMap是基於hash表的實現,HashMap是非線程安全的,ConcurrentHashMap是線程安全的,會鎖Map的部分結構,HashTable也是線程安全的,但性能比ConcurrentHashMap查,HashTable會鎖整個map對象。

HashMap是一個用於存儲key-value鍵值對的集合,每一個鍵值對也叫做Entry,這些鍵值對(Entry)分散存儲在一個數組當中,整個數組就是HashMap的主幹,HashMap數組每個元素的初始值都是NUll

先來看一段代碼吧

public static void main(String[] args){
	// 實例化一個HashMap,這裏說一下,HashMap默認初始長度是16,並且每次擴展或手動初始化時,長度必須是2的冪(原因下面解釋)
	Map<String, Object> map = new HashMap();
    // 向map中添加元素(put原理下面會講到)
    map.put("張三key","張三value");
    map.put("李四key","李四value");
    map.put("王五key","王五value");
    map.put("趙六key","趙六value");
	
	System.out.println("map.size() : " + map.size());
	// 通過get方法獲取map中“李四key”對應的value值
	System.out.println("李四key: " + map.get("李四key"));
}

執行構造函數,當我們看到new時,應該就能想到在內存中開闢了一塊空間
Map<String, Object> map = new HashMap();

構造函數源碼如下:
在這裏插入圖片描述
初始化了一個負載因子,負載因子默認爲0.75,爲啥是0.75
初始化了一個負載因子
看到這裏明白了,原來HashMap是數組,數組裏的對象是Node
在這裏插入圖片描述
下面我們看看Node是什麼,這裏是部分源碼

static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;	//key,value用來存儲put時的key,value
        V value;
        Node<K,V> next; //next 用來標記下一個元素,這個跟鏈表很像

		// Node的構造函數
        Node(int hash, K key, V value, Node<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }
    }

接下來我們看看他的兩個成員變量,size是map的長度,modCount用來記錄修改次數
在這裏插入圖片描述
現在我們來總結一下一個HashMap對象總包含哪些東西,包含了一個Node類型的數組,一個記錄長度的size,一個負載因子default_load_factor,還有一個記錄修改次數的modCount

來看一下HashMap初始化的圖吧
在這裏插入圖片描述
HashMap初始化好後,成員變量table數組默認爲null,size默認爲0,負載因子默認爲0.75,往裏添加元素,使用的是put()方法,我們來看下源碼

public V put(K key, V value) {

	// 調用了putVal方法,key,value是傳入的
    return putVal(hash(key), key, value, false, true);
}

// putVal方法的源碼
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
		// 放入第一個元素時,tab爲空,觸發resize方法
        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);
        else {
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

resize方法比較複雜,就不完全貼出來了,當放入第一個元素時,會觸發resize方法的以下關鍵代碼(當放入第一個元素時,如果底層數組還是null,系統會初始化一個長度爲16的Node數組)

// 將HashMap的長度設爲默認長度
newCap = DEFAULT_INITIAL_CAPACITY;

// HashMap的默認長度,“<<” 位移運算法,1<<4相當於16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

// 將new出來的數組返回,雖然數組長度爲16,但是HashMap的邏輯長度仍然是0,這隻表示他的容量爲16
return newTab;

接着看圖
在這裏插入圖片描述
繼續看putVal的方法
在這裏插入圖片描述

if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);

這段代碼看着比較費勁,可以重寫下

i = (n - 1) & hash;  // hash是傳過來的,n是底層數組的長度,用&運算符計算出i的值
p = tab[i];  //從數組中取元素
if(p == null){ 
// 如果這個元素爲null,用key,value構造一個Node對象放入數組下標爲i的位置
	tab[i] = new Node(hash,key,value,null);
}

hash值是key的hashCode方法與HashMap提供的hash()方法共同計算出來的記過,其中n是數組的長度,目前這個數組的長度爲16,不管這個hash值是多少,經過(n-1) & hash計算出阿里的值一定在n-1之間,用這個i作爲下標,去取值,如果爲null,創建一個Node放到數組下標爲i的位置。

hash部分的源碼

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

繼續添加元素,李四,王五,趙六一切正常,key:“李四”經過(n-1)&hash算出來的數組下標爲1,王五爲7,趙六爲9,如圖:
在這裏插入圖片描述
繼續添加“孫七”,通過(n-1)&hash計算“孫七”這個key時,計算出來的下標值是1,而數組下標1這個位置已經被“李四”佔了,產生了衝突,下面我們看看HashMap是怎麼解決衝突的

在這裏插入圖片描述
上圖紅框裏就是衝突的處理邏輯,這一句是關鍵

  p.next = newNode(hash, key, value, null);

也就是說new一個新的Node對象,並把當前Node的next引用指向該對象,也就是說原來該位置上只有一個元素對象,現在轉成了單鏈表,接着看圖
在這裏插入圖片描述
紅框中海油兩行比較重要的代碼

if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
      treeifyBin(tab, hash);

// 可以看下TREEIFY_THRESHOLD的值
static final int TREEIFY_THRESHOLD = 8;

當鏈表長度到8時,將鏈表轉化爲紅黑樹來處理,接着看圖
在這裏插入圖片描述
在jdk1.7及以前的版本中,HashMap中沒有紅黑樹的實現,在jdk1.8中加入了紅黑樹是爲了防止哈希表碰撞共計,當鏈表長度爲8時及時轉爲紅黑樹,提高map的效率。

這裏來說下爲啥HashMap數組的長度應該爲16或2的冪,而不是10或者其他值

i = (n - 1) & hash
當長度不是16或2的冪的時候,有的index結果出現的機率會更大,而有些index永遠不會出現,不符合hash算法均勻分佈的原則。

如果長度爲16或2的冪,length-1的值是所有二進制位全爲1,這種情況下,index的結果就等同於HashCode後幾位的值,只要輸入的hashCode本身分佈均勻,hash算法的結果就是均勻的。
詳細的解釋大家可以看程序員小灰的文章,奉上連接:
https://zhuanlan.zhihu.com/p/31610616

讓我們來假設下面試場景

面試官:你能說說Java的常用數據結構-HashMap的底層實現原理麼?
答: HashMap是由key,value鍵值對 數組組成的,默認長度爲16,也可以自己設置,可以通過put方法來存放元素,get方法獲取元素
put方法,根據key 的hash值來計算該key,value在數組中的index,假設我們存儲key爲a的元素,hash(a)的值爲1,那麼就會把她存儲在index=1的位置,接着存儲key爲b的元素,我們假設hash(b)的值也爲1,index重複後,該位置會存儲鏈表,將b插入到單向鏈表的頭元素,並頭節點的next指針指向a,當鏈表長度達到8時,在jdk8中又會轉爲紅黑樹,來提高map存儲和讀取的效率;
get方法,根據key的hash值爲來計算index,從數組中獲取元素,如果該元素存儲的就是該key,value,那麼取出,如果是鏈表則查看頭元素是不是,如果不是就根據next向下尋找,鏈表沒找到的話,根據紅黑樹的規則來找(左節點一律小於父節點,右節點一律大於父節點,節點爲紅色,黑色,不能有連續的紅色,紅色節點的子節點都爲黑色,任一節點到其葉子節點的路徑上黑色節點的數量一樣)。

面試官:hashMap的默認長度爲什麼是16
答:HashMap的默認長度爲16,而且我們一般建議如果設置長度的話設爲2的冪次方,原因是len-1的所有二進制位全爲1,這種情況下hash(key) &(len-1) 的結果等同於HashCode的後幾位,只要輸入的hashCode均勻些,index就會均勻些,而其他值就有可能會出現有的index出現的機率大,有的幾乎不出現;

面試官:說下高併發下HashMap可能存在哪些問題吧?
答:哇,這個超綱了,等我看了程序員小灰的文章再來回答吧。。。
面試官:嗯???

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