Java容器系列--HashMap源碼閱讀

HashMap

版本:1.8

隨便new一個HashMap然後進入其中.

文中不復制過多的源碼,可以比較着看.

1.結構分析

1.1 變量:

所有的變量都在這裏了

/**
 * defaultInitialCapacity
 * 默認初始容量(16),
 * 用來新建map容器時作爲參考.
 */
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;

/**
 * maximumCapacity
 * 最大容量(1<<30)
 */
static final int MAXIMUM_CAPACITY = 1 << 30;

/**
 * defaultLoadFactory
 * 默認加載因子(0.75)
 */
static final float DEFAULT_LOAD_FACTOR = 0.75f;

/**
 * treeifyThreshold
 * 樹起點.(8)
 * 由鏈表轉化爲紅黑樹存儲的閾值.
 */
static final int TREEIFY_THRESHOLD = 8;

/**
 * unireeifyThreshold
 * 不是樹的起點.(6)
 * 轉化爲鏈表存除的閾值.
 */
static final int UNTREEIFY_THRESHOLD = 6;

/**
 * minTreeifyCapacity
 * 最小樹容量(64)
 * 當桶中的結構轉爲紅黑樹的最小table大小.
 * 當table<64 && hash衝突太多時,改用擴容操作,而不是轉化爲紅黑樹存除.
 */
static final int MIN_TREEIFY_CAPACITY = 64;

/**
 * 下面四個變量都用的transient修飾,表示的是不需要進行序列化.
 * 都是臨時存在的.
 */
//保存Node節點的數組,在首次使用時初始化,並根據需求調整大小.
//我們文中提到的桶指的就是該數組.
//這個桶表示的是鏈表||紅黑樹起始節點的數組,比如三個<K,V>,可能只有兩個在table中.其中一個因爲衝突,在以table位起點的鏈表中.
transient Node<K,V>[] table;

//存放元素的集合
transient Set<Map.Entry<K,V>> entrySet;

//記錄hashMap的大小
transient int size;

//mod的計數器,用來修改map結構.
transient int modCount;

//臨界值 當實際大小(容量*加載因子)超過臨界值時,進行擴容
int threshold;

//加載因子.在擴容的時候需要,計算threshold=(CAPACITY*loadFactory)
final float loadFactor;

1.2 內部類

可以看出來大概分了三類:

  • 迭代器的一類:iteratorSpliterator各4個.
  • 賦值的一類:key,value,entry有3個.
  • 內部結構的一類:NodeTreeNode分別表示鏈表結構和紅黑樹結構.

迭代器和賦值先忽略,我們重點分析一下結構.

1.2.1關於Node<K,V>

自己查看源碼可發現.實現了Map.Entry<K,V>接口.是一個單項鍊表(只有一個next).

1.2.2 關於TreeNode<K,V>.

繼承了LinkedHashMap.Entry<K,V>類.實現方法有很多.通過其中的變量可以看出來是一個紅黑樹map.

然後我們通過從構造,到增刪改查,再到調整結構來分析一下各個參數的作用.

1.3 工具方法

HashMap中有一些基本的方法.

  • hash(Object key):我們重新得到了一個Object的哈希值:通過高16位和低16位異或運算得出的哈希值.
  • 求位置運算:我們在源碼中常見的一個運算是((n-1)&hash).這個運算的目的是爲了通過對桶的大小取模,找出該元素所對應的哈希桶的位置.然後再進一步的尋找.

2. 流程分析.

2.1 構造方法

HashMap中重載了4個構造方法.可以看出來,都是設置loadFactorythreshold屬性,這個屬性就是我們上面提到的加載因子初始容量大小.

然後轉化爲合法的大小.比如容量只能是2的整次冪.通過一些運算將容量合法.

PS:這個獲取合法初始容量大小的方法tableSizeFor也小優化了一下.通過1,2,4,8,16的五次位運算.就可以求出最小的值.確實厲害.(有興趣的自行了解一下,並不難)

2.2 查找.

查找就是普通的查找方法,沒什麼難度的.

可以看出來,當我們調用get(Object key)方法後,

  1. 首先通過hash(Object key)獲得了這個key的哈希值,
  2. 然後通過哈希值和這個key調用了真正的查找函數getNode(int hash,Object key).
  3. 這個查找函數經過查找後返回一個<K,V>對象.然後我們再返回value.

進入getNode()函數

  1. tab是用來確定長度,first確定位置,e是目標鍵值對.
  2. 首先找到鍵值對所在桶的位置.((n-1)&hash)是對hash取模,因爲位運算要快於模運算.
  3. 判斷桶中第一個元素是否是我們要找的.如果是的話就返回first.
  4. 桶中不止一個節點的話,通過instanceof關鍵字判斷一下是否是紅黑樹,如果是的話,通過getTreeNode來找,否則直接遍歷這個鏈表然後查找.

TreeNode中的getTreeNode.

  1. 如果當前節點是根節點,就直接在當前所在的紅黑樹中調用find,否則找到根節點調用find.
  2. 在紅黑樹中也是,左右子節點伸下去.,找到目標節點就返回TreeNode對象.

總之,查找就是根據hash和key來判斷

2.3 添加

首先找到put(K key,V value)函數.看到了它調用了putVal函數,並且傳入了hash值.

putVal中分析:

  1. 先定義了變量,tab表示桶,p表示要添加的元素.n表示的數組長度.
  2. 在第一個if中我們知道,先判斷了桶中元素是否爲空,如果爲空的話需要進行擴容操作resize(),然後重新賦值tabn.
  3. 通過求位置運算(上文中介紹了)可以得到該元素在桶中的位置,如果沒有發生hash碰撞,就直接將這個點存進去,否則進行else步驟.
  4. else中:hash值相同,但是key不同的時候,表示的是該元素在鏈表||紅黑樹中.
    1. 我們定義了兩個變量:e表示的是要修改的目標節點,k表示的是key.
    2. 首先進行了一個判斷起使位置是否是目標節點.如果是目標節點直接得到e.
    3. 然後判斷是紅黑樹節點還是鏈表.紅黑樹調用putTreeVal方法.
    4. 如果是鏈表遍歷鏈表,
      1. 如果不存在key,就在末尾添加一個.然後判斷一下是否需要樹化treeifBin().
      2. 如果遍歷中存在key,直接獲取e.
    5. 最後如果e不爲空,表示的就是原HashMap中存在一個相同key的節點e,我們將其修改一下即可.
  5. 所有操作都做完後,判斷一下是否需要擴容(size>threshold).

可以看出來,putVal()函數主要乾了4件事:

  1. table合理.
  2. 向對應的位置放元素,如果是鏈表則放入鏈表,如果是紅黑樹則放入紅黑樹.
  3. 鏈表的如果需要轉化爲紅黑樹的轉化爲紅黑樹.
  4. 再進行擴容.

流程用一個網圖來說明:侵刪https://upload-images.jianshu.io/upload_images/5982616-c1b9d345bc371797?imageMogr2/auto-orient/strip|imageView2/2/format/webp
看一下擴容函數resize().

  1. 定義了幾個變量.oldTab表示舊的table表(當第一次添加元素的時候位null).oldCap表示舊的容量,oldThr表示舊的擴容閾值,然後定義新的容量newCap和新的閾值newThr.
  2. 第一個判斷:合法的擴容處理.
    1. 如果oldCap>0,則將新的容量和閾值都擴大爲原來的2倍.
      1. 如果已經擴到了MAXINUM_CAPACITY,則擴容閾值設置爲Integer.MAX_VALUE.不進行擴容.直接返回oldTab.
      2. 否則看一下是否到了最大容量(newCap=oldCap<<1)且設置新的閾值newThr=oldThr<<1.
      3. 如果都不是則不設置新的閾值.
    2. 如果oldThr>0,表示我們在構造函數中設置了初始的大小.直接更新newCap=oldThr即可.
    3. 否則表示第一此初始化容器,使用默認參數構造newCapnewThr.
  3. 第二個判斷:如果新的擴容閾值爲空,表示當前table爲空,但是有閾值.
    1. 計算新閾值ft=newCap*loadFactory.
    2. 如果新的容量小於2^30&&ft<2^30.就將ft賦值給newThr.否則爲Integer.MAX_Value.
  4. 我們獲得了所有的更新後的容量和閾值之後.開始進行擴容.
  5. 申請一個新的容量的newTab,將其賦值給全局的table.
  6. 如果老數組不爲空我們則需要進行賦值操作.
    1. 遍歷每個老數組中每個位置的鏈表或者紅黑樹,重新計算節點位置,然後插入新數組.
    2. 對於每一個oldTab中存在元素的位置.我們先釋放原數組中的元素.
    3. 如果此時,該節點是一個獨立的(e.next==null),則直接計算新的桶中的下標(e.hash&*(newCap-1)).插入其中.
    4. 如果該節點爲紅黑樹節點,則需要根據split()函數進一步確定該節點在新數組中的位置.
    5. 否則就是拷貝鏈表.
      1. 定義了四個節點,分別是低位頭尾和高位頭尾.
        爲什麼會有低位和高位之分?
        我們對原數組進行擴容,我們的容量增大一倍,那麼我們原來衝突的hash值現在可能不衝突了.分成了高位和低位兩種情況.
      2. 這裏的e.hash&oldCap可以判斷出該節點應存放的位置.等於0表示在低位,大於0表示放在高位.然後重新放入鏈表
      3. 將所有的元素都放完後,分別判斷低位和高位的Tail.並且將其Head放入newTab中.
  7. 最後返回newTab.

可以看出來,擴容函數resize()主要乾了兩件事:

  1. 獲得一個新的合理的容量newCap和閾值newThr.
  2. 申請完新的table後進行值的拷貝.注意了高位和低位的分開.

2.4 修改

HashMap中沒有update等操作.我們可以通過put()來修改.

2.5 刪除

查看remove(Object key)函數.調用removeNode()函數,返回刪除節點的value.

注意這裏有兩個remove().分別是remove(Object key)remove(Object key,Object value).觀察發現其區別就在於調用removeNode()函數中參數matchValuemovable.

matchValuetrue表示只有當Value和傳入的value相同時才刪除節點.

movabletrue表示刪除

查看removeNode():

  1. 前半部分和我們的查找類似.顯示找到目標節點node.
  2. 接下來判斷是否是我們要刪除的節點.查看matchValue參數比較.
  3. 如果是紅黑樹節點,調用removeTreeNode()函數.
  4. 然後將這個節點刪除,向後推一位.
  5. 更新modeCountsize.

參考資料

HashMap源碼分析:https://www.jianshu.com/p/2e2a18d02218

容器類框架分析-HashMap源碼分析:https://www.jianshu.com/p/4f7add8ed8f5

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