HashMap的幾個構造方法解析
HashMap的幾個構造方法原始碼解析(基於JDK1.8中的HashMap原始碼)
1、無參構造方法HashMap()
/** * Constructs an empty <tt>HashMap</tt> with the default initial capacity * (16) and the default load factor (0.75). */ public HashMap() { //無參構造器 //負載因子為預設值 0.75f //容量為預設初始值 16 this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted }
2、有一個初始容量引數的構造方法HashMap(int initialCapacity)
引數:initialCapacity 初始容量
/** * Constructs an empty <tt>HashMap</tt> with the specified initial * capacity and the default load factor (0.75). * * @param initialCapacity the initial capacity. * @throws IllegalArgumentException if the initial capacity is negative. */ public HashMap(int initialCapacity) { //此處通過把第二個引數負載因子使用預設值0.75f,然後呼叫有兩個引數的構造方法 this(initialCapacity, DEFAULT_LOAD_FACTOR); }
這個一個引數的構造方法,使用HashMap的預設負載因子,把該初始容量和預設負載因子作為入參,呼叫HashMap的兩個引數的構造方法
3、有兩個引數的構造方法HashMap(int initialCapacity, float loadFactor)
引數:initialCapacity 初始容量
引數:loadFactor 負載因子
/** * Constructs an empty <tt>HashMap</tt> with the specified initial * capacity and load factor. * 通過指定的初始容量和負載因子初始化一個空的HashMap * * @param initialCapacity the initial capacity 初始化容量 * @param loadFactor the load factor 負載因子 * @throws IllegalArgumentException if the initial capacity is negative * or the load factor is nonpositive * 如果初始容量或者負載因子為負數,則會丟擲非法資料異常 */ public HashMap(int initialCapacity, float loadFactor) { if (initialCapacity < 0) //如果初始容量小於0,丟擲異常 throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); if (initialCapacity > MAXIMUM_CAPACITY) //如果初始容量超過最大容量(1<<32) initialCapacity = MAXIMUM_CAPACITY; //則使用最大容量作為初始容量 if (loadFactor <= 0 || Float.isNaN(loadFactor)) //如果負載因子小於等於0或者不是數字,則丟擲異常 throw new IllegalArgumentException("Illegal load factor: " + loadFactor); this.loadFactor = loadFactor; //把負載因子賦值給成員變數loadFactor //呼叫tableSizeFor方法計算出不小於initialCapacity的最小的2的冪的結果,並賦給成員變數threshold this.threshold = tableSizeFor(initialCapacity); }
我們下面看看tableSizeFor()這個方法是如何計算的,這個方法的實現原理很巧妙,原始碼如下:
/**
* Returns a power of two size for the given target capacity.
*/
static final int tableSizeFor(int cap) {
int n = cap - 1; //容量減1,為了防止初始化容量已經是2的冪的情況,最後有+1運算。
n |= n >>> 1; //將n無符號右移一位再與n做或操作
n |= n >>> 2; //將n無符號右移兩位再與n做或操作
n |= n >>> 4; //將n無符號右移四位再與n做或操作
n |= n >>> 8; //將n無符號右移八位再與n做或操作
n |= n >>> 16; //將n無符號右移十六位再與n做或操作
//如果入參cap為小於或等於0的數,那麼經過cap-1之後n為負數,n經過無符號右移和或操作後仍未負
//數,所以如果n<0,則返回1;如果n大於或等於最大容量,則返回最大容量;否則返回n+1
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
借用一位博主對tableSizeFor()方法的梳理:該演算法分析原文地址(https://blog.csdn.net/fan2012huan/article/details/51097331)
首先,為什麼要對cap做減1操作。int n = cap - 1;
這是為了防止,cap已經是2的冪。如果cap已經是2的冪, 又沒有執行這個減1操作,則執行完後面的幾條無符號右移操作之後,返回的capacity將是這個cap的2倍。如果不懂,要看完後面的幾個無符號右移之後再回來看看。
下面看看這幾個無符號右移操作:
如果n這時為0了(經過了cap-1之後),則經過後面的幾次無符號右移依然是0,最後返回的capacity是1(最後有個n+1的操作)。
這裡只討論n不等於0的情況。
第一次右移
n |= n >> 1;
由於n不等於0,則n的二進位制表示中總會有一bit為1,這時考慮最高位的1。通過無符號右移1位,則將最高位的1右移了1位,再做或操作,使得n的二進位制表示中與最高位的1緊鄰的右邊一位也為1,如000011xxxxxx。
第二次右移
n |= n >>> 2;
注意,這個n已經經過了n |= n >>> 1; 操作。假設此時n為000011xxxxxx ,則n無符號右移兩位,會將最高位兩個連續的1右移兩位,然後再與原來的n做或操作,這樣n的二進位制表示的高位中會有4個連續的1。如00001111xxxxxx 。
第三次右移
n |= n >>> 4;
這次把已經有的高位中的連續的4個1,右移4位,再做或操作,這樣n的二進位制表示的高位中會有8個連續的1。如00001111 1111xxxxxx 。
以此類推
注意,容量最大也就是32bit的正數,因此最後n |= n >>> 16; ,最多也就32個1,但是這時已經大於了MAXIMUM_CAPACITY ,所以取值到MAXIMUM_CAPACITY 。
舉一個例子說明下吧。
這個演算法著實牛逼啊!
注意,得到的這個capacity卻被賦值給了threshold。
this.threshold = tableSizeFor(initialCapacity);
開始以為這個是個Bug,感覺應該這麼寫:
this.threshold = tableSizeFor(initialCapacity) * this.loadFactor;
這樣才符合threshold的意思(當HashMap的size到達threshold這個閾值時會擴容)。
但是,請注意,在構造方法中,並沒有對table這個成員變數進行初始化,table的初始化被推遲到了put方法中,在put方法中會對threshold重新計算。
4、有一個Map型別的引數的構造方法
/**
* Constructs a new <tt>HashMap</tt> with the same mappings as the
* specified <tt>Map</tt>. The <tt>HashMap</tt> is created with
* default load factor (0.75) and an initial capacity sufficient to
* hold the mappings in the specified <tt>Map</tt>.
* 根據傳入的指定的Map引數去初始化一個新的HashMap,該HashMap擁有著和原Map中相同的對映關係
* 以及預設的負載因子(0.75f)和一個大小充足的初始容量
* @param m the map whose mappings are to be placed in this map
* 引數 m 一個對映關係將會被新的HashMap所取代的Map
* @throws NullPointerException if the specified map is null
* 如果這個Map為空的話,將會丟擲空指標異常
*/
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR; //將預設的負載因子賦值給成員變數loadFactor
putMapEntries(m, false); //呼叫PutMapEntries()來完成HashMap的初始化賦值過程
}
我們看下putMapEntries()方法,這個方法呼叫了HashMap的resize()擴容方法和putVal()存入資料方法,原始碼如下:
/**
* Implements Map.putAll and Map constructor
*
* @param m the map
* @param evict false when initially constructing this map, else
* true (relayed to method afterNodeInsertion).
*/
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
int s = m.size(); //定義一個s,大小等於map的大小,這個未做非空判斷,可能丟擲空指標異常
if (s > 0) { //如果map鍵值對個數大於0
if (table == null) { // pre-size 如果當前的HashMap的table為空
float ft = ((float)s / loadFactor) + 1.0F; //計算HashMap的最小需要的容量
int t = ((ft < (float)MAXIMUM_CAPACITY) ? //如果該容量大於最大容量,則使用最大容量
(int)ft : MAXIMUM_CAPACITY);
if (t > threshold) //如果容量大於threshold,則對對容量計算,取大於該容量的最小的2的冪的值
//並賦給threshold,作為HashMap的容量。tableSizeFor()方法講解在上面
//已經有了說明,不明白的小夥伴可以往上翻翻看。
threshold = tableSizeFor(t);
}
else if (s > threshold) //如果table不為空,即HashMap中已經有了資料,判斷Map的大小是否超過了HashMap的閾值
resize(); //如果超過閾值,則需要對HashMap進行擴容
for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) { //對Map的EntrySet進行遍歷
K key = e.getKey();
V value = e.getValue();
putVal(hash(key), key, value, false, evict); //呼叫HashMap的put方法的具體實現方法來對資料進行存放。
}
}
}
要看懂putMapEntries()方法,就必須弄懂resize()方法和putVal()方法,下面我們對這兩個方法原始碼進行分析。
我們下面看看resize方法的原始碼:
/**
* Initializes or doubles table size. If null, allocates in
* accord with initial capacity target held in field threshold.
* Otherwise, because we are using power-of-two expansion, the
* elements from each bin must either stay at same index, or move
* with a power of two offset in the new table.
* 初始化或把table容量翻倍。如果table是空,則根據threshold屬性的值去初始化HashMap的容
* 量。如果不為空,則進行擴容,因為我們使用2的次冪來給HashMap進行擴容,所以每個箱子裡的元素
* 必須保持在原來的位置或在新的table中以2的次冪作為偏移量進行移動
*
*
* @return the table
*/
final Node<K,V>[] resize() {
//定義一個oldTab存放當前的table
Node<K,V>[] oldTab = table;
//判斷當前的table是否為空,如果為空,則把0賦值給新定義的oldCap,否則以table的長度作為oldCap的大小
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold; //把table的閾值賦值給oldThr變數
int newCap, newThr = 0; //定義變數newCap和newThr來存放新的table的容量和閾值
if (oldCap > 0) { //如果原來的table長度大於0
if (oldCap >= MAXIMUM_CAPACITY) { //判斷長度是否大於HashMap的最大容量
threshold = Integer.MAX_VALUE; //以int的最大值作為原來HashMap的閾值,並把原來的table返回
return oldTab;
}
//如果原table容量不超過HashMap的最大容量,將 原容量*2 賦值給變數newCap,如果newCap不大於HashMap的最大容量,並且原容量大於HashMap的預設容量
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
//將newThr的值設定為 原HashMap的閾值*2
newThr = oldThr << 1; // double threshold
}
//如果原容量不大於0,即原table為null,並且原閾值大於0
else if (oldThr > 0) // initial capacity was placed in threshold
//將原閾值作為容量賦值給newCap當做newCap的值
newCap = oldThr;
// 如果原容量不大於0,別切原閾值也不大於0
else { // zero initial threshold signifies using defaults
//則以預設容量作為newCap的值
newCap = DEFAULT_INITIAL_CAPACITY;
//以初始容量*預設負載因子的結果作為newThr值
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
//經過上面的處理過程,如果newThr值為0,給newThr進行賦值
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
//將新的閾值newThr賦值給threshold,為新初始化的HashMap來使用
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
//初始化一個新的容量大小為newCap的Node陣列
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
//給table重新賦值
table = newTab;
if (oldTab != null) { //如果原來的HashMap中有值,則遍歷
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) { //如果原來的table陣列中第j個位置不為空
oldTab[j] = null; //把e = oldTab[j],然後讓oldTab[j]置空
if (e.next == null) //如果e.next = null,說明e.next不存在其他Node
newTab[e.hash & (newCap - 1)] = e; //此時以e.hash&(newCap-1)的結果作為e在newTab中的位置
else if (e instanceof TreeNode) //否則判斷e的型別是TreeNode還是Node,即連結串列和紅黑樹判斷
((TreeNode<K,V>)e).split(this, newTab, j, oldCap); //如果時紅黑樹,則進行紅黑樹的處理
else { // preserve order //如果是連結串列
//定義了五個Node變數,我一直想知道lo和hi是是哪兩個單詞的縮寫,
//根據程式碼來看應該是lower和higher吧,也就是高位和低位,
//因為我們知道HashMap擴容時,容量會擴到原容量的2倍,
//也就是放在連結串列中的Node的位置可能保持不變或位置變成 原位置+oldCap ,
//這裡的高低應該就是這個意思吧,當然這只是個人理解。
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do { //迴圈連結串列中的Node
next = e.next;
//如果e.hash & oldCap == 0,注意這裡是oldCap,而不是oldCap-1。
//我們知道oldCap是2的次冪,也就是1、2、4、8、16...轉化為二進位制之後,
//都是高位為1,其它位為0。所以oldCap & e.hash 也是隻有e.hash值在oldCap二進位制不為0的位對應的位也不為0時,
//才會得到一個不為0的結果。舉個例子,我們知道10010 和00010 與1111的&運算結果都是 0010 ,
//但是110010和010010與10000的運算結果是不一樣的,所以HashMap就是利用這一點,
//來判斷當前在連結串列中的資料,在擴容時位置時保持不變還是位置移動oldCap。
if ((e.hash & oldCap) == 0) { //如果結果為0,即位置保持不變
if (loTail == null) //如果是第一次遍歷
loHead = e; //讓loHead = e
else
loTail.next = e; //否則,讓loTail的next = e
loTail = e; //最後讓loTail = e
}
//其實if 和else 中做的事情是一樣的,我們看到有loHead和loTail兩個Node,
//我們其實可以把loHead當做頭元素,然後loTail是用來維護loHead的,即每次迴圈,
//更新loHead的next。我們來舉個例子,比如原來的連結串列是A->B->C->D->E。
//我們這裡把->假設成next關係,這五個Node中,只有C的hash & oldCap != 0 ,
//然後這個程式碼執行過程就是:
//第一次迴圈: 先拿到A,把A賦給loHead,然後loTail也是A
//第二次迴圈: 此時e的為B,而且loTail != null,也就是進入上面的else分支,把loTail.next =
// B,此時loTail中即A->B,同樣反應在loHead中也是A->B,然後把loTail = B
//第三次迴圈: 此時e = C,由於C不滿足 (e.hash & oldCap) == 0,進入到了我們下面的else分支,其
// 實做的事情和當前分支的意思一樣,只不過維護的是hiHead和hiTail。
//第四次迴圈: 此時e的為D,loTail != null,進入上面的else分支,把loTail.next =
// D,此時loTail中即B->D,同樣反應在loHead中也是A->B->D,然後把loTail = D
//.
//.
//.
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
//遍歷結束,即把table[j]中所有的Node處理完
if (loTail != null) { //如果loTail不為空,此時保證了loHead不為空
loTail.next = null; //此時把loTail的next置空
newTab[j] = loHead; //把loHead放在newTab陣列的第j個位置上
}
if (hiTail != null) { 同理,只不過hiHead放的位置是j+oldCap
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab; //最後返回newTab
}
我們上面講了resize()方法,下面看看putVal的原始碼:
/**
* Implements Map.put and related methods
*
* @param hash hash for key
* @param key the key
* @param value the value to put
* @param onlyIfAbsent if true, don't change existing value
* @param evict if false, the table is in creation mode.
* @return previous value, or null if none
*/
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) //如果hashMap為空,則使用resize方法去初始化HashMap
n = (tab = resize()).length; //把table陣列的長度賦值給n
if ((p = tab[i = (n - 1) & hash]) == null) //如果tab的第(n-1) & hash為空,即此處沒有Node
tab[i] = newNode(hash, key, value, null); //則初始化一個新的Node存放在此處
else {
Node<K,V> e; K k; //如果需要存放的位置已經存在了鍵值對
if (p.hash == hash && //判斷此處鍵值對的key是否和我們要存入的鍵值對的key相同
((k = p.key) == key || (key != null && key.equals(k))))
e = p; //如果相同,則把此處的Node賦給Node e
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) { //如果當前節點的next為空,即當前連結串列中不存在我們要存放的鍵值對
p.next = newNode(hash, key, value, null); //則把當前節點的next賦值為我們要存放的鍵值對
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)))) //如果連結串列中存在鍵值對的key和我們要存入的鍵值對的key相同的Node,則跳出迴圈,此時e = p.next
break;
p = e; //把p = p.next 進行遍歷
}
}
if (e != null) { // existing mapping for key //如果e不為空,此處的e為我們要存放value的鍵值對
V oldValue = e.value; //把原來的值取出來
if (!onlyIfAbsent || oldValue == null) //如果onlyIfAbsent為false或者oldValue為null則進行覆蓋,預設onlyIfAbsent為false
e.value = value;
afterNodeAccess(e); //這個為linkedHashMap中才有意義,HashMap為空方法
return oldValue;
}
}
++modCount; //讓HashMap的修改次數+1
if (++size > threshold) //判斷當前Hash的鍵值對數量是否超過擴容閾值
resize(); //如果超過擴容閾值則進行擴容
afterNodeInsertion(evict); //這個為linkedHashMap中才有意義,HashMap為空方法
return null;
}
終於分析完了,自己也從中獲益很多,希望看到的同學也能從中獲益。2018-12-20 今天英雄聯盟德瑪西亞杯哦~