由於主要說的是1.8版本中的實現。而1.8中HashMap是數組+鏈表+紅黑樹實現的,大概以下圖所示。後面仍是主要介紹Hash Map中主要的一些成員以及方法原理。html
那麼上述圖示中的結點Node具體類型是什麼,源碼以下。Node是HashMap的內部類,實現了Map.Entery接口,主要就是存放咱們put方法所添加的元素。其中的next就表示這能夠構成一個單向鏈表,這主要是經過鏈地址法解決發生hash衝突問題。而當桶中的元素個數超過閾值的時候就換轉爲紅黑樹。java
//hash桶中的結點Node,實現了Map.Entry
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next; //鏈表的next指針
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
public final K getKey() { return key; }
public final V getValue() { return value; }
public final String toString() { return key + "=" + value; }
//重寫Object的hashCode
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
//equals方法
public final boolean equals(Object o) {
if (o == this)
return true;
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}
//轉變爲紅黑樹後的結點類
static final class TreeNode<k,v> extends LinkedHashMap.Entry<k,v> {
TreeNode<k,v> parent; // 父節點
TreeNode<k,v> left; //左子樹
TreeNode<k,v> right;//右子樹
TreeNode<k,v> prev; // needed to unlink next upon deletion
boolean red; //顏色屬性
TreeNode(int hash, K key, V val, Node<k,v> next) {
super(hash, key, val, next);
}
//返回當前節點的根節點
final TreeNode<k,v> root() {
for (TreeNode<k,v> r = this, p;;) {
if ((p = r.parent) == null)
return r;
r = p;
}
}
}
複製代碼
上面只是大概瞭解了一下HashMap的簡單組成,下面主要介紹其中的一些參數和重要的方法原理實現。node
//默認初始化容量初始化=16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
//最大容量 = 1 << 30
static final int MAXIMUM_CAPACITY = 1 << 30;
//默認加載因子.通常HashMap的擴容的臨界點是當前HashMap的大小 > DEFAULT_LOAD_FACTOR *
//DEFAULT_INITIAL_CAPACITY = 0.75F * 16
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//當hash桶中的某個bucket上的結點數大於該值的時候,會由鏈表轉換爲紅黑樹
static final int TREEIFY_THRESHOLD = 8;
//當hash桶中的某個bucket上的結點數小於該值的時候,紅黑樹轉變爲鏈表
static final int UNTREEIFY_THRESHOLD = 6;
//桶中結構轉化爲紅黑樹對應的table的最小大小
static final int MIN_TREEIFY_CAPACITY = 64;
//hash算法,計算傳入的key的hash值,下面會有例子說明這個計算的過程
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
//tableSizeFor(initialCapacity)返回大於initialCapacity的最小的二次冪數值。下面會有例子說明
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
//hash桶
transient Node<K,V>[] table;
//保存緩存的entrySet
transient Set<Map.Entry<K,V>> entrySet;
//桶的實際元素個數 != table.length
transient int size;
//擴容或者更改了map的計數器。含義:表示這個HashMap結構被修改的次數,結構修改是那些改變HashMap中的映射數量或者
//修改其內部結構(例如,從新散列rehash)的修改。 該字段用於在HashMap失敗快速(fast-fail)的Collection-views
//上建立迭代器。
transient int modCount;
//臨界值,當實際大小(cap*loadFactor)大於該值的時候,會進行擴充
int threshold;
//加載因子
final float loadFactor;
複製代碼
//hash算法
static final int hash(Object key) {
int h;
//key == null : 返回hash=0
//key != null
//(1)獲得key的hashCode:h=key.hashCode()
//(2)將h無符號右移16位
//(3)異或運算:h ^ h>>>16
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
複製代碼
假設如今咱們向一個map中添加元素,例如map.put("fsmly","test"),那麼其中key爲"fsmly"的hashCode的二進制表示爲0000_0000_0011_0110_0100_0100_1001_0010,按照上面的步驟來計算,那麼咱們調用hash算法獲得的hash值爲:算法
該方法的做用就是:返回大於initialCapacity的最小的二次冪數值。以下實例shell
//n=cap-1=5; 5的二進制0101B。>>> 操做符表示無符號右移,高位取0
//n |= n>>>1: (1)n=0101 | 0101>>>1; (2)n=0101 | 0010; (3)n = 0111B
//n |= n>>>2: (1)n=0111 | 0111>>>2; (2)n=0111 | 0011; (3)n = 0111B
//n |= n>>>4: (1)n=0111 | 0111>>>4; (2)n=0111 | 0000; (3)n = 0111B
//n |= n>>>8: (1)n=0111 | 0111>>>8; (2)n=0111 | 0000; (3)n = 0111B
//n |= n>>>16:(1)n=0111 | 0111>>>16;(2)n=0111 | 0000; (3)n = 0111B
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
//n<0返回1
//n>最大容量,返回最大容量
//不然返回n+1(0111B+1B=1000B=8)
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
複製代碼
再看下面這個:數組
//至於這裏爲何減1,當傳入的cap爲2的整數次冪的時候,減1即保證最後的計算結果仍是cap,而不是大於cap的另外一個2的
//整數次冪,例如咱們傳入cap=16=10000B.按照上面那樣計算
//n=cap-1=15=1111B.按照上面的方法計算獲得:
// n |= n>>>1: n=1111|0111=1111;後面仍是相同的結果最後n=1111B=15.
//因此返回的時候爲return 15+1;
int n = cap - 1;
複製代碼
咱們看看HashMap源碼中爲咱們提供的四個構造方法。咱們能夠看到,日常咱們最經常使用的無參構造器內部只是僅僅初始化了loadFactor,別的都沒有作,底層的數據結構則是延遲到插入鍵值對時再進行初始化,或者說在resize中會作。後面說到擴容方法的實現的時候會講到。緩存
//(1)參數爲初始化容量和加載因子的構造函數
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity); //閾值爲大於initialCapacity的最小二次冪
}
//(2)只給定初始化容量,那麼加載因子就是默認的加載因子:0.75
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
//(3)加載因子爲默認的加載因子,可是這個時候的初始化容量是沒有指定的,後面調用put或者get方法的時候才resize
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
//(4)將傳遞的map中的值調用putMapEntries加入新的map集合中,其中加載因子是默認的加載因子
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
複製代碼
其中第(4)中構造方法是將傳入的map集合中的元素添加到本map實例中,源碼以下數據結構
//該函數將傳遞的map集合中的全部元素加入本map實例中
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
int s = m.size(); //m.size()
if (s > 0) {
//若是本map實例的table爲null,沒有初始化,那麼須要初始化
if (table == null) { // pre-size
//實際大小:ft = m.size() / 0.75 + 1;
float ft = ((float)s / loadFactor) + 1.0F;
//判斷剛剛計算的大小是否小於最大值1<<<30
int t = ((ft < (float)MAXIMUM_CAPACITY) ?
(int)ft : MAXIMUM_CAPACITY);
//計算的實際大小ft大於當前的閾值threshhold,那麼將threshhold從新計算,tableSizeFor傳遞的
//參數是計算的大小,即從新計算大於ft的最小二次冪
if (t > threshold)
threshold = tableSizeFor(t);
}
//若是table!=null,而且m.size() > threshhold,直接進行擴容處理
else if (s > threshold)
resize();
//將map中的全部元素加入本map實例中
for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
K key = e.getKey();
V value = e.getValue();
putVal(hash(key), key, value, false, evict);
}
}
}
複製代碼
無論增長、刪除、查找鍵值對,定位到哈希桶數組的索引都是很關鍵的第一步,因此咱們看看源碼怎樣經過hash()方法以及其餘代碼肯定一個元素在hash桶中的位置的。多線程
//計算map中key的hash值
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
//這一小段代碼就是定位元素在桶中的位置。具體作法就是:容量n-1 & hash.
//其中n是一個2的整數冪,而(n - 1) & hash其實質就是hash%n,但
//是取餘運算的效率不如位運算與,而且(n - 1) & hash也能保證散列均勻,不會產生只有偶數位有值的現象
p = tab[i = (n - 1) & hash];
複製代碼
下面咱們經過一個例子計算一下上面這個定位的過程,假設如今桶大小n爲16.app
咱們能夠看到,這裏的hash方法並非用原有對象的hashcode最爲最終的hash值,而是作了必定位運算,大概由於若是(n-1)的值過小的話,(n - 1) & hash的值就徹底依靠hash的低位值,好比n-1爲0000 1111,那麼最終的值就徹底依賴於hash值的低4位了,這樣的話hash的高位就玩徹底失去了做用,h ^ (h >>> 16),經過這種方式,讓高位數據與低位數據進行異或,也是變相的加大了hash的隨機性,這樣就不單純的依賴對象的hashcode方法了。
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
//table == null 或者table的長度爲0,調用resize方法進行擴容
//這裏也說明:table 被延遲到插入新數據時再進行初始化
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 這裏就是調用了Hash算法的地方,具體的計算可參考後面寫到的例子
//這裏定位座標的作法在上面也已經說到過
if ((p = tab[i = (n - 1) & hash]) == null)
// 若是計算獲得的桶下標值中的Node爲null,就新建一個Node加入該位置(這個新的結點是在
//table數組中)。而該位置的hash值就是調用hash()方法計算獲得的key的hash值
tab[i] = newNode(hash, key, value, null);
//這裏表示put的元素用本身key的hash值計算獲得的下表和桶中的第一個位置元素產生了衝突,具體就是
//(1)key相同,value不一樣
//(2)只是經過hash值計算獲得的下標相同,可是key和value都不一樣。這裏處理的方法就是鏈表和紅黑樹
else {
Node<K,V> e; K k;
//上面已經計算獲得了該hash對應的下標i,這裏p=tab[i]。這裏比較的有:
//(1)tab[i].hash是否等於傳入的hash。這裏的tab[i]就是桶中的第一個元素
//(2)比較傳入的key和該位置的key是否相同
//(3)若是都相同,說明是同一個key,那麼直接替換對應的value值(在後面會進行替換)
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
//將桶中的第一個元素賦給e,用來記錄第一個位置的值
e = p;
//這裏判斷爲紅黑樹。hash值不相等,key不相等;爲紅黑樹結點
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);
//前面的binCount是記錄鏈表長度的,若是該值大於8,就會轉變爲紅黑樹
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
//若是在遍歷鏈表的時候,判斷得出要插入的結點的key和鏈表中間的某個結點的key相
//同,就跳出循環,後面也會更新舊的value值
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
//e = p.next。遍歷鏈表所用
p = e;
}
}
//判斷插入的是否存在HashMap中,上面e被賦值,不爲空,則說明存在,更新舊的鍵值對
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value; //用傳入的參數value更新舊的value值
afterNodeAccess(e);
return oldValue; //返回舊的value值
}
}
//modCount修改
++modCount;
//容量超出就擴容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
複製代碼
能夠看到主要邏輯在put方法中調用了putVal方法,傳遞的參數是調用了hash()方法計算key的hash值,主要邏輯在putVal中。能夠結合註釋熟悉這個方法的執行,我在這裏大概總結一下這個方法的執行:
首先 (tab = table) == null || (n = tab.length) == 0這一塊判斷hash桶是否爲null,若是爲null那麼會調用resize方法擴容。後面咱們會說到這個方法
定位元素在桶中的位置,具體就是經過key的hash值和hash桶的長度計算獲得下標i,若是計算到的位置處沒有元素(null),那麼就新建結點而後添加到該位置。
若是table[i]處不爲null,已經有元素了,那麼就代表產生hash衝突,這裏多是三種狀況
①判斷key是否是同樣,若是key同樣,那麼就將新的值替換舊的值;
②若是不是由於key同樣,那麼須要判斷當前該桶是否是已經轉爲了紅黑樹,是的話就構造一個TreeNode結點插入紅黑樹;
③不是紅黑樹,就使用鏈地址法處理衝突問題。這裏主要就是遍歷鏈表,若是在遍歷過程當中也找到了key同樣的元素,那麼久仍是使用新值替換舊值。不然會遍歷到鏈表結尾處,到這裏就直接新添加一個Node結點插入鏈表,插入以後還須要判斷是否是已將超過了轉換爲紅黑樹的閾值8,若是超過就會轉爲紅黑樹。
最後須要修改modCount的值。
判斷插入後的size大小是否是超過了threshhold,若是超過須要進行擴容。
上面不少地方都涉及到了擴容,因此下面咱們首先看看擴容方法。
擴容(resize)就是從新計算容量,具體就是當map內部的size大於DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY ,就須要擴大數組的長度,以便能裝入更多的元素。resize方法實現中是使用一個新的數組代替已有的容量小的數組。
//該方法有2種使用狀況:1.初始化哈希表(table==null) 2.當前數組容量太小,需擴容
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table; //oldTab指向舊的table數組
//oldTab不爲null的話,oldCap爲原table的長度
//oldTab爲null的話,oldCap爲0
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold; //閾值
int newCap, newThr = 0;
if (oldCap > 0) {
//這裏代表oldCap!=0,oldCap=原table.length();
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE; //若是大於最大容量了,就賦值爲整數最大的閥值
return oldTab;
}
// 若是數組的長度在擴容後小於最大容量 而且oldCap大於默認值16(這裏的newCap也是在原來的
//長度上擴展兩倍)
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold雙倍擴展threshhold
}
else if (oldThr > 0) // initial capacity was placed in threshold
// 這裏的oldThr=tabSizeFor(initialCapacity),從上面的構造方法看出,若是不是調用的
//無參構造,那麼threshhold確定都會是通過tabSizeFor運算獲得的2的整數次冪的,因此能夠將
//其做爲Node數組的長度(我的理解)
newCap = oldThr;
else { // zero initial threshold signifies using defaults(零初始閾值表示使用默認值)
//這裏說的是咱們調用無參構造函數的時候(table == null,threshhold = 0),新的容量等於默
//認的容量,而且threshhold也等於默認加載因子*默認初始化容量
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// 計算新的resize上限
if (newThr == 0) {
float ft = (float)newCap * loadFactor; //容量 * 加載因子
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
//以新的容量做爲長度,建立一個新的Node數組存放結點元素
//固然,桶數組的初始化也是在這裏完成的
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
//原來的table不爲null
if (oldTab != null) {
// 把每一個bucket都移動到新的buckets中
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
//原table中下標j位置不爲null
if ((e = oldTab[j]) != null) {
oldTab[j] = null; //將原來的table[j]賦爲null,及時GC?
if (e.next == null) //若是該位置沒有鏈表,即只有數組中的那個元素
//經過新的容量計算在新的table數組中的下標:(n-1)&hash
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
//若是是紅黑樹結點,從新映射時,須要對紅黑樹進行拆分
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // 鏈表優化重hash的代碼塊
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {//上面判斷不是紅黑樹,那就是鏈表,這裏就遍歷鏈表,進行從新映射
next = e.next;
// 原位置
if ((e.hash & oldCap) == 0) {
//loTail處爲null,那麼直接加到該位置
if (loTail == null)
loHead = e;
//loTail爲鏈表尾結點,添加到尾部
else
loTail.next = e;
//添加後,將loTail指向鏈表尾部,以便下次從尾部添加
loTail = e;
}
// 原位置+舊容量
else {
//hiTail處爲null,就直接點添加到該位置
if (hiTail == null)
hiHead = e;
//hiTail爲鏈表尾結點,尾插法添加
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
// 將分組後的鏈表映射到新桶中
// 原索引放到bucket裏
if (loTail != null) {
//舊鏈表遷移新鏈表,鏈表元素相對位置沒有變化;
//實際是對對象的內存地址進行操做
loTail.next = null;//鏈表尾元素設置爲null
newTab[j] = loHead; //數組中位置爲j的地方存放鏈表的head結點
}
// 原索引+oldCap放到bucket裏
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
複製代碼
我這裏添加上一點,就是爲何使用 (e.hash & oldCap) == 0判斷是處於原位置仍是放在更新的位置(原位置+舊容量),解釋以下:咱們知道capacity是2的冪,因此oldCap爲10...0的二進制形式(好比16=10000B)。
(1)若判斷條件爲真,意味着oldCap爲1的那位對應的hash位爲0(1&0=0,其餘位都是0,結果天然是0),對新索引的計算沒有影響,至於爲啥沒影響下面就說到了。先舉個例子計算一下數組中的下標在擴容先後的變化:
從上面計算髮現,當cap爲1的那位對應的hash爲0的時候,resize先後的index是不變的。咱們再看下面,使用上面的hash值,對應的就是 (e.hash & oldCap) == 0,剛好也是下標不變的
(2)若判斷條件爲假,則 oldCap爲1的那位對應的hash位爲1。好比新下標=hash&( newCap-1 )= hash&( (16<<2) - 1)=10010,至關於多了10000,即 oldCap .如同下面的例子
從上面計算髮現,當cap爲1的那位對應的hash爲1的時候,resize先後的index是改變的。咱們再看下面,使用上面的hash值,對應的就是 (e.hash & oldCap) != 0,剛好下標就是原索引+原容量
這一部分其實和put方法中,使用鏈地址法解決hash衝突的原理差很少,都是對鏈表的操做。
// 原位置
if ((e.hash & oldCap) == 0) {
//loTail處爲null,那麼直接加到該位置
if (loTail == null)
loHead = e;
//loTail爲鏈表尾結點,添加到尾部
else
loTail.next = e;
//添加後,將loTail指向鏈表尾部,以便下次從尾部添加
loTail = e;
}
// 原位置+舊容量
else {
//hiTail處爲null,就直接點添加到該位置
if (hiTail == null)
hiHead = e;
//hiTail爲鏈表尾結點,尾插法添加
else
hiTail.next = e;
hiTail = e;
}
複製代碼
咱們直接經過一個簡單的圖來理解吧
resize代碼稍微長了點,可是總結下來就是這幾點
基本邏輯就是根據key算出hash值定位到哈希桶的索引,當能夠就是當前索引的值則直接返回其對於的value,反之用key去遍歷equal該索引下的key,直到找到位置。
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
//計算存放在數組table中的位置.具體計算方法上面也已經介紹了
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
//先查找是否是就是數組中的元素
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
//該位置爲紅黑樹根節點或者鏈表頭結點
if ((e = first.next) != null) {
//若是first爲紅黑樹結點,就在紅黑樹中遍歷查找
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
//不是樹結點,就在鏈表中遍歷查找
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
複製代碼
參考https://coolshell.cn/articles/9606.html