HashMap源碼入門學習(JDK1.8)

hashMap數據結構

    hashMap採用數組+鏈表的存儲方式,數組的每個元素存儲的是一個Node節點,該節點是一個靜態內部類,有4個屬性,如圖:
Node節點

  • hash :用於存儲hash算法計算出來的hash值,該結果需要參與數組下標的計算(下面會介紹)
  • key : 程序put的Key值
  • value:程序put的Value值
  • next:記錄鏈表的下一個節點指向

    數組在hashMap類中已聲明,這裏順便介紹一下其他常量:
transient Node<K,V>[] table;   // Node數組
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // hashMap默認的初始容量爲16
static final int MAXIMUM_CAPACITY = 1 << 30; // 最大的容量 2^30
static final float DEFAULT_LOAD_FACTOR = 0.75f;  // 負載因子(當存儲的數量達到容量X該值的結果,則進行擴容)
static final int TREEIFY_THRESHOLD = 8; // 從jdk1.8開始,當鏈表長度達到8時,則將鏈表轉爲紅黑樹
static final int UNTREEIFY_THRESHOLD = 6; // 與上面相反,當紅黑樹的元素個數減少到6時,將紅黑樹轉爲鏈表

草圖:
struct

put方法到底做了什麼?

源碼所示

put方法
這裏有個hash(key)方法,這個方法就是計算hash值的方法,繼續點進去
hash
    這裏爲什麼要將計算出來的hash值右移16位然後與原hash值做異或運算呢?因爲object的hashCode()方法計算出來的是一個32位的整型,如果將該值向右移動16位,然後再與原hash值做運算,就相當於將該整型的高16位與低16位進行異或運算,這樣就可以使得每一位都能參與運算,使得結果值更加隨機,這就是所謂的Hash算法

putVal方法

源碼如圖:
putVal
分析:

  • 初始化數組
if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;

這是第一次添加時,table爲空,則需要初始化,繼續查看resize()方法,resize方法的用途除了初始化還有2倍擴容的功能,由於源碼比較繁瑣,鄙人只截取相關代碼大概說一下流程:

  1. 獲取原來數組的容量和臨界值:當原容量大於0,①.如果原來的容量已達到最大值,則不進行擴容,直接將其返回;②.如果沒有達到最大值並且原容量大於等於默認值(16),則將舊的臨界值擴大2倍作爲新的臨界值

  2. 在原容量等於0的情況下,如果原臨界值大於0(如果使用無參的構造函數,該值爲16),則將擴容後數組的初始容量設置爲原臨界值,否則,新的容量爲16(默認),新的臨界值爲12(默認),這也是第一次put的時候程序進行的操作,

  3. 將臨界值更新,看吧這裏可能說的不太清楚,上圖看吧
    在這裏插入圖片描述

  4. 進行擴容
    由於是2倍擴容且容量是2^n,所以源碼在這用了一個非常巧妙的方法,歎爲觀止啊,不多BB,來看看,
    在這裏插入圖片描述假設原來的容量爲16,hash值的二進制表示1001110100100011000111100100001,則原來下標爲2 :在這裏插入圖片描述
    擴容後容量爲32, 此時由於hash值的倒數第五位是0,則源碼中的(e.hash & oldCap) == 0 成立(注意此時的oldCap還是爲16),這就意味着,(32-1)的二進制與hash值進行與運算的結果還是2(與原來下標相同)。如圖:在這裏插入圖片描述
    如果倒數第五位是1,則擴容後計算的結果是18(10010),也就是 新的下標 = 原來下標+原容量,不需要用與運算的方法重新計算下標位置了

  • 計算數組對應下標的值
if ((p = tab[i = (n - 1) & hash]) == null)
   tab[i] = newNode(hash, key, value, null);

這裏的(n-1 & hash)是很巧妙的,n是Node數組的長度,hash是用上面的hash算法計算出來的hash值,假如此時計算出來的hash值的二進制表示10011101001000110001111001000010 爲設數組的長度是16,則n-1=15,則(n-1&hash)在這裏插入圖片描述
則數組下標爲2,(這裏爲啥不用隨機數呢,因爲隨機數不能保證數組的每個位置都能較好的利用到),同時,這裏也可以看出,map的容量大小應爲2^n, 假設容量不爲2的正數次冪,就不能保證與運算的結果很好的隨機,因爲n-1的結果“1”串中會含有“0”,導致同hash與運算的結果受到影響,這麼說吧,我辛辛苦苦計算出雨露均沾的hash值,你這一進行與運算,又給我弄的不受我hash值控制,我很難受啊!!

  • 添加值
           if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;   // 如果鍵值對已存在,在代替舊的值,並且最終會將該舊的值返回
            else if (p instanceof TreeNode)
            	// 如果手機紅黑樹的存儲方式,則進行相關操作,PS:鄙人看的有點懵逼,不想看了!23333333
                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);
                        // 鏈表長度到達8時,轉紅黑樹
                        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;
                }
            }

暫時寫到這,鄙人也剛開始學,望批評指正,共勉。。。。

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