一起看源碼:深入HashMap


簡介: 採用數組+鏈表 或者 數組+紅黑樹方式進行元素的存儲。
存儲在 hashMap集合中的元素都將是一個 Map.Entry 的內部接口的實現。
當數組的下標位是鏈表時,此時存儲在該下標位置的內容將是Map.Entry 的一個實現 Node 內部類對象。
當數組的下標位是紅黑樹時,此時存儲在該下標位置的內容將是 Map.Entry 的一個實現 TreeNode 內部類對象。

繼承結構

在這裏插入圖片描述
AbstractMap:繼承於 Map 的抽象類,它實現了 Map 中的大部分 API。其它 Map 的實現類 可以通過繼承 AbstractMap 來減少重複編碼

認識紅黑樹

說HashMap前,先簡單的認識下紅黑樹的概念:
1.紅黑樹一種特殊的平衡二叉查找樹.。
2.紅黑樹在數據的插入和刪除上比平衡二叉樹(AVL)效率更高 。
3.紅黑樹在數據的查詢上由於可能存在樹的高度比 AVL 樹高一層的情況,查詢性能略差一點。

注意:選擇紅黑樹是查詢和增刪性能的折中選擇
紅黑樹 5大規則:
1.每個節點都用一個標誌位來標識或者是黑色,或者是紅色。
2.根節點是一定是黑色。
3.每個葉子節點(NIL 節點)是黑色。
4.如果一個節點是紅色的,則它的子節點必須是黑色的。
5.從一個節點到該節點的子孫節點的所有路徑上包含相同數目的黑節點

HashMap

HashMap 重要的成員屬性:

在這裏插入圖片描述

說明:
int DEFAULT_INITIAL_CAPACITY = 1 << 4:hashMap的默認數組長度是 1左位移4位就是1*2^4=16

在這裏插入圖片描述

說明:
float DEFAULT_LOAD_FACTOR = 0.75f:擴容的平衡因子0.75。

在這裏插入圖片描述

說明:
int TREEIFY_THRESHOLD = 8:鏈表轉換紅黑樹的元素個數,數組內元素大於8就會由鏈表轉爲紅黑樹

在這裏插入圖片描述

說明:
int UNTREEIFY_THRESHOLD = 6:紅黑樹轉爲鏈表的元素個數,數組內元素個數到6時就會由紅黑樹轉爲鏈表

在這裏插入圖片描述

說明:
Node<K,V>[] table:存儲元素的數組對象,底層是Entry<K,V>
注意:Node<K,V>是HashMap中的靜態內部類,實現Map類中的Entry<K,V>接口

在這裏插入圖片描述

說明:‘
size:集合中元素的個數
modCount:用於FailFast機制,類似於版本號(詳見文章:深入List 分支 ArrayList)

put(K key, V value)過程

1.找到put(K key, V value)方法
在這裏插入圖片描述
2.找到hash(Object key)方法,傳入key
這方法就是將key進行了hash運算並返回

注意:
key==null時默認爲0,這也就是爲什麼hashMap可以將null作爲key來存儲
key!=null時就將key的hashcode值進行高低十六位的異或運算

在這裏插入圖片描述
3.進入putVal方法:
這裏主要就看源碼中添加元素的key不重複的情況:
注意:註釋是我自己手寫的,代碼爲源代碼

 final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;	//不得不說,設計者真是能省一行是一行啊
        if ((tab = table) == null || (n = tab.length) == 0)  //table的定義上邊已經介紹到了,存儲元素的數組對象
            n = (tab = resize()).length; //這裏就是初始化容量的地方,第一次存儲元素時,數組肯定爲null或者長度爲0。
        if ((p = tab[i = (n - 1) & hash]) == null) //存儲下標確立的地方,
            tab[i] = newNode(hash, key, value, null); //該下標位沒有元素時(也就是key不重複時),存儲元素的地方
        else {
        //這裏就是key重複的情況,省略。。。
         }
        ++modCount;
        if (++size > threshold) //當前集合中元素的個數+1 > 擴容的閾值時進行擴容(threshold在resize()中給予賦值:當前數組長度*0.75)
            resize(); //動態擴容的地方
        afterNodeInsertion(evict);
        return null;

存儲下標確立過程

上邊說到了存儲下標確立的地方,那麼過程具體是什麼呢?

說明:
n:看上邊中n = (tab = resize()).length;其實就是初始化數組的長度也就是16(這裏假設爲第一次添加元素)
hash:就是傳入的hash值,也就是int hash(Object key)方法將key經過hash計算得出得值
假設傳入得key=“czy”,經hash(Object key)方法計算返回得hash值爲99043
i = (n - 1) & hash爲(16-1) & 99043 = 3即i=3
那麼該元素得下標即爲3

> 順帶說一下與運算,以上述爲例: 
> 15 & 99043 將其轉換爲二進制即爲1111	& 1 1000 0010 1110 0011> 計算方式:兩位同時爲“1”,結果才爲“1”,否則爲0
> 1 1000 0010 1110 0011> 0 0000 0000 0000 1111
> ---------------------------------
> 0 0000 0000 0000 0011
> 計算結果爲:0011轉換爲十進制爲3

在這裏插入圖片描述

HashMap的初始化與動態擴容

HashMap的初始化與動態擴容都在resize()這個方法,這裏只看初始化與動態擴容相關的代碼,代碼中的註釋都是手寫,其他代碼省略。

注意:
初始化在第一次進行添加元素時。
動態擴容:比如說當前的容器容量是16,負載因子是0.75。16*0.75=12,也就是說,當集合中元素數量達到了12的時候就會進行擴容操作。
這裏的擴容操作假設爲第一次擴容

 final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;  //將原來的數組對象賦值給oldTab,初始化時table爲null即oldTab爲null。
        int oldCap = (oldTab == null) ? 0 : oldTab.length; //初始化時oldCap肯定爲0,否則爲數組的長度
        int oldThr = threshold;//擴容的閾值賦值給oldThr,這裏假設第一次擴容,閾值爲12(16*0.75)
        int newCap, newThr = 0;
        if (oldCap > 0) { //進行擴容的操作
            if (oldCap >= MAXIMUM_CAPACITY) { //int MAXIMUM_CAPACITY 定義爲1 << 30,數組長度怎麼可能大於1*2^30,所以說基本上是進不到這個if語句的
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            //那麼來到這裏,假設爲第一次擴容oldCap = 16:newCap= 16 << 1 = 32 < 1*2^30  && 16 >= 16(DEFAULT_INITIAL_CAPACITY),條件滿足,進入if語句:newThr  = oldThr << 1 即 12 << 1 = 24
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY) 
                newThr = oldThr << 1; // double threshold
        }
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        else {               // zero initial threshold signifies using defaults
        //這裏進行了初始化的操作
            newCap = DEFAULT_INITIAL_CAPACITY; //newCap = 16;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); //newThr = 0.75 * 16 = 12
        }
		threshold = newThr; //threshold = 12即第一次擴容的閾值爲12
        @SuppressWarnings({"rawtypes","unchecked"})
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; // 創建一個數組對象 長度爲newCap
        table = newTab; //將新建的數組對象賦值給table
		if (oldTab != null) {
			//此處省略的操作爲:
			//如果集合中有數據時,進行擴容後的操作:
			//判斷元素數據結構時紅黑樹還是鏈表,並根據對應的操作將原來的元素添加到新的數組中去,並對下標進行處理
		}
 	return newTab;
 }

理解get(Object key)的過程

該方法通過傳入的key找出對應的值

1.找到get(Object key)方法
hash(key)這個方法上邊已經詳細說過,就是根據key計算出一個hash值,直接下一步進入getNode(int hash, Object key)方法
在這裏插入圖片描述
2.進入getNode(int hash, Object key)方法,傳入通過該key計算出的hash值和key值

final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k; 
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {  //如果當前數組不爲null,並且數組元素個數>0,將通過確立下標得到的元素賦值給first ,並且first不爲null時,進入該if語句
                    if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))//如果first對象中保存的hash值等於通過當前key計算出的hash值並且first對象中保存的key值與用戶傳入的key值相同時,就進入該if語句,返回first對象
                return first;
            if ((e = first.next) != null) { //走到這就說明hash值不相等,或者key值不相同
                if (first instanceof TreeNode) //如果first對象數據結構是紅黑樹,就以紅黑樹的方式獲取值節點對象,這裏不再深入了。
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                    
                do {//如果程序走到這裏,就說明first節點對象的數據結構是鏈表,就以鏈表的方式獲取節點對象,並返回。
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;//說明沒有該key對應的值唄,返回null
    }

3.返回到get(Object key)方法中

return (e = getNode(hash(key), key)) == null ? null : e.value;

如果getNode(int hash, Object key)方法返回的是null 那麼get方法就返回null值
如果getNode(int hash, Object key)方法返回的不爲null即是一個Node<K,V>節點,那麼就返回該節點對象的value值。

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