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 內部類
可以看出來大概分了三類:
- 迭代器的一類:
iterator
和Spliterator
各4個. - 賦值的一類:
key,value,entry
有3個. - 內部結構的一類:
Node
和TreeNode
分別表示鏈表結構和紅黑樹結構.
迭代器和賦值先忽略,我們重點分析一下結構.
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個構造方法.可以看出來,都是設置loadFactory
和threshold
屬性,這個屬性就是我們上面提到的加載因子
和初始容量大小
.
然後轉化爲合法的大小.比如容量只能是2的整次冪.通過一些運算將容量合法.
PS:這個獲取合法初始容量大小的方法tableSizeFor
也小優化了一下.通過1,2,4,8,16
的五次位運算.就可以求出最小的值.確實厲害.(有興趣的自行了解一下,並不難)
2.2 查找.
查找就是普通的查找方法,沒什麼難度的.
可以看出來,當我們調用get(Object key)
方法後,
- 首先通過
hash(Object key)
獲得了這個key
的哈希值, - 然後通過哈希值和這個
key
調用了真正的查找函數getNode(int hash,Object key)
. - 這個查找函數經過查找後返回一個
<K,V>
對象.然後我們再返回value
.
進入getNode()
函數
tab
是用來確定長度,first
確定位置,e
是目標鍵值對.- 首先找到鍵值對所在桶的位置.
((n-1)&hash)
是對hash取模,因爲位運算要快於模運算. - 判斷桶中第一個元素是否是我們要找的.如果是的話就返回
first
. - 桶中不止一個節點的話,通過
instanceof
關鍵字判斷一下是否是紅黑樹,如果是的話,通過getTreeNode
來找,否則直接遍歷這個鏈表然後查找.
在TreeNode
中的getTreeNode
.
- 如果當前節點是根節點,就直接在當前所在的紅黑樹中調用
find
,否則找到根節點調用find
. - 在紅黑樹中也是,左右子節點伸下去.,找到目標節點就返回
TreeNode
對象.
總之,查找就是根據hash和key來判斷
2.3 添加
首先找到put(K key,V value)
函數.看到了它調用了putVal
函數,並且傳入了hash值.
在putVal
中分析:
- 先定義了變量,
tab
表示桶,p
表示要添加的元素.n
表示的數組長度. - 在第一個if中我們知道,先判斷了桶中元素是否爲空,如果爲空的話需要進行擴容操作
resize()
,然後重新賦值tab
和n
. - 通過
求位置運算(上文中介紹了)
可以得到該元素在桶中的位置,如果沒有發生hash碰撞
,就直接將這個點存進去,否則進行else
步驟. else
中:hash值相同,但是key不同的時候,表示的是該元素在鏈表||紅黑樹中.- 我們定義了兩個變量:
e
表示的是要修改的目標節點,k
表示的是key. - 首先進行了一個判斷起使位置是否是目標節點.如果是目標節點直接得到
e
. - 然後判斷是紅黑樹節點還是鏈表.紅黑樹調用
putTreeVal
方法. - 如果是鏈表遍歷鏈表,
- 如果不存在key,就在末尾添加一個.然後判斷一下是否需要樹化
treeifBin()
. - 如果遍歷中存在key,直接獲取
e
.
- 如果不存在key,就在末尾添加一個.然後判斷一下是否需要樹化
- 最後如果
e
不爲空,表示的就是原HashMap
中存在一個相同key的節點e
,我們將其修改一下即可.
- 我們定義了兩個變量:
- 所有操作都做完後,判斷一下是否需要擴容
(size>threshold)
.
可以看出來,putVal()
函數主要乾了4件事:
- 將
table
合理. - 向對應的位置放元素,如果是鏈表則放入鏈表,如果是紅黑樹則放入紅黑樹.
- 鏈表的如果需要轉化爲紅黑樹的轉化爲紅黑樹.
- 再進行擴容.
流程用一個網圖來說明:侵刪
看一下擴容函數resize()
.
- 定義了幾個變量.
oldTab
表示舊的table表
(當第一次添加元素的時候位null
).oldCap
表示舊的容量,oldThr
表示舊的擴容閾值,然後定義新的容量newCap
和新的閾值newThr
. - 第一個判斷:合法的擴容處理.
- 如果
oldCap>0
,則將新的容量和閾值都擴大爲原來的2倍.- 如果已經擴到了
MAXINUM_CAPACITY
,則擴容閾值設置爲Integer.MAX_VALUE
.不進行擴容.直接返回oldTab
. - 否則看一下是否到了最大容量
(newCap=oldCap<<1)
且設置新的閾值newThr=oldThr<<1
. - 如果都不是則不設置新的閾值.
- 如果已經擴到了
- 如果
oldThr>0
,表示我們在構造函數中設置了初始的大小.直接更新newCap=oldThr
即可. - 否則表示第一此初始化容器,使用默認參數構造
newCap
和newThr
.
- 如果
- 第二個判斷:如果新的擴容閾值爲空,表示當前table爲空,但是有閾值.
- 計算新閾值
ft=newCap*loadFactory
. - 如果
新的容量小於2^30&&ft<2^30
.就將ft
賦值給newThr
.否則爲Integer.MAX_Value
.
- 計算新閾值
- 我們獲得了所有的更新後的容量和閾值之後.開始進行擴容.
- 申請一個新的容量的
newTab
,將其賦值給全局的table
. - 如果老數組不爲空我們則需要進行賦值操作.
- 遍歷每個老數組中每個位置的鏈表或者紅黑樹,重新計算節點位置,然後插入新數組.
- 對於每一個
oldTab
中存在元素的位置.我們先釋放原數組中的元素. - 如果此時,該節點是一個獨立的
(e.next==null)
,則直接計算新的桶中的下標(e.hash&*(newCap-1))
.插入其中. - 如果該節點爲紅黑樹節點,則需要根據
split()
函數進一步確定該節點在新數組中的位置. - 否則就是拷貝鏈表.
- 定義了四個節點,分別是低位頭尾和高位頭尾.
爲什麼會有低位和高位之分?
我們對原數組進行擴容,我們的容量增大一倍,那麼我們原來衝突的hash值現在可能不衝突了.分成了高位和低位兩種情況. - 這裏的
e.hash&oldCap
可以判斷出該節點應存放的位置.等於0表示在低位,大於0表示放在高位.然後重新放入鏈表 - 將所有的元素都放完後,分別判斷低位和高位的
Tail
.並且將其Head
放入newTab
中.
- 定義了四個節點,分別是低位頭尾和高位頭尾.
- 最後返回
newTab
.
可以看出來,擴容函數resize()
主要乾了兩件事:
- 獲得一個新的合理的容量
newCap
和閾值newThr
. - 申請完新的
table
後進行值的拷貝.注意了高位和低位的分開.
2.4 修改
HashMap
中沒有update等操作.我們可以通過put()
來修改.
2.5 刪除
查看remove(Object key)
函數.調用removeNode()
函數,返回刪除節點的value.
注意這裏有兩個remove()
.分別是remove(Object key)
和remove(Object key,Object value)
.觀察發現其區別就在於調用removeNode()
函數中參數matchValue
和movable
.
matchValue
爲true
表示只有當Value
和傳入的value
相同時才刪除節點.
movable
爲true
表示刪除
查看removeNode()
:
- 前半部分和我們的查找類似.顯示找到目標節點
node
. - 接下來判斷是否是我們要刪除的節點.查看
matchValue
參數比較. - 如果是紅黑樹節點,調用
removeTreeNode()
函數. - 然後將這個節點刪除,向後推一位.
- 更新
modeCount
和size
.
參考資料
HashMap源碼分析:https://www.jianshu.com/p/2e2a18d02218
容器類框架分析-HashMap源碼分析:https://www.jianshu.com/p/4f7add8ed8f5