JAVA之HashMap源碼分析(詳細註釋)

1. 初次看源碼的童鞋,需要靜下心來一步步走,多嘗試幾次就可以,一定要穩住。

2. 先理解什麼是HashMap

在JDK1.7和之前,HashMap使用的數據結構是數組+ 鏈表,JDK1.7之後,使用的數據結構是數組+ 鏈表/紅黑樹,紅黑樹的插入,查找,刪除等操作,平均複雜度均爲O(logn)。

哈希表添加,刪除,查找等操作,性能十分之高,不考慮哈希衝突,時間複雜度爲O(1)即可完成。一般是我們key,通過hash函數f進行映射,取餘後得到數組下標(有時候也叫做桶下標)進行插入,如:key --> f (key) % n --> index。當遇到hash衝突的時候,一般解決方法是線性探針法和拉鍊法(也稱鏈地址法),HashMap中hash衝突使用拉鍊法。

結構圖如圖所示 : 傳送門
在這裏插入圖片描述

2.源碼分析,戳進HashMap類中。源碼先從構造搞起

2.1 在分析構造函數前,先看看定義的幾個關鍵變量

// 默認容量爲16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; 

// 最大容量爲2^30
static final int MAXIMUM_CAPACITY = 1 << 30; 
// 有人會問爲什麼不設置成1<<31?因爲有符號整形表示的範圍爲-2^31~2^31-1,因此溢出爲最小負值。
//那爲什麼是不是(1<<31)-1?因爲hash運算過程中要求擴容大小爲2的n次冪,從而減小哈希碰撞,-1不滿足冪次運算

// 也就是說大小爲16的HashMap,到了第13個元素,就會擴容成32。
static final float DEFAULT_LOAD_FACTOR = 0.75f; 
// 桶上鍊接的節點數大於等於8鏈表轉成紅黑樹
static final int TREEIFY_THRESHOLD = 8;
// 桶上鍊接的節點數小於等於6退化成鏈表
static final int UNTREEIFY_THRESHOLD = 6; 
// 最小的紅黑樹容量,允許存在64個節點,大於則調整
static final int MIN_TREEIFY_CAPACITY = 64; 
// 定義裝填因子
final float loadFactor; 
// 爲capacity*loadFactory,此時大於threshold則進行擴容
int threshold; 

2.2 4種構造方法

  1. 無參構造方法
public HashMap() {
	this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
} 
  1. 有容量參數的構造方法
public HashMap(int initialCapacity) {
	// 其實調用了HashMap(int initialCapacity, float loadFactor) 
	this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
  1. 帶容量參數和裝填因子的構造方法
public HashMap(int initialCapacity, float loadFactor) {
	if (initialCapacity < 0)
	    throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);
	 // 如果初始容量大於最大容量,則等於最大容量
	if (initialCapacity > MAXIMUM_CAPACITY)
	    initialCapacity = MAXIMUM_CAPACITY;
	 // 如果參數裝填因子小於0或者裝填因子是非浮點型,拋出參數非法
	if (loadFactor <= 0 || Float.isNaN(loadFactor))
	    throw new IllegalArgumentException("Illegal load factor: " + loadFactor);
	this.loadFactor = loadFactor;
	 // 調用tableSizeFor,運算過程如下
	this.threshold = tableSizeFor(initialCapacity);
}

其中調用了tableSizeFor()方法,返回一個值大於cap且最接近cap的2正數的冪次。

static final int tableSizeFor(int cap) {
    // 防止當前容量已經滿足2的冪次
    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;
}

這段代碼移位操作比較麻煩,我自己debug並參考了其他人文章才理解,建議手動模擬操作一下就十分好理解。爲什麼這麼操作,參考一篇博客:傳送門

第一次右移
n |= n >>> 1;
由於n不等於0,則n的二進制表示中總會有一bit爲1,這時考慮最高位的1。通過無符號右移1位,則將最高位的1右移了1位,再做或操作,使得n的二進制表示中與最高位的1緊鄰的右邊一位也爲1,如0000 1xxx xxxx xxxx

第二次右移
n |= n >>> 2;
注意,這個n已經經過了n |= n >>> 1; 操作。假設此時n爲0000 11xx xxxx xxxx,則n無符號右移兩位,會將最高位兩個連續的1右移兩位,然後再與原來的n做或操作,這樣n的二進制表示的高位中會有4個連續的1。如0000 1111 xxxx xxxx

第三次右移
n |= n >>> 4;
這次把已經有的高位中的連續的4個1,右移4位,再做或操作,這樣n的二進制表示的高位中會有8個連續的1。如0000 1111 1111 xxxx

最後
n |= n >>> 16; ,最多也就32個1,可以保證最高位後面的全部置爲1,但是這時已經大於了MAXIMUM_CAPACITY ,所以取值到MAXIMUM_CAPACITY 。
————————————————
在這裏插入圖片描述

  1. 調用參數爲map類型的構造方法,其實做的就是一個新的深拷貝(debug就可以看到,變動原map,新的構造方法map值不變)
public HashMap(Map<? extends K, ? extends V> m) {
	this.loadFactor = DEFAULT_LOAD_FACTOR;
	// 構造一個和指定Map有相同類型的HashMap,其實就是參數的深拷貝
	putMapEntries(m, false);
}

其中調用了putMapEntries()方法,源碼如下

final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
	// 傳入的參數m的大小
	int s = m.size();
	if (s > 0) {
	    // 如果table沒有被初始化
	    if (table == null) { // pre-size
	        // 初始化新的HashMap容量
	        float ft = ((float)s / loadFactor) + 1.0F;
	        // t表示真正需要的容量
	        int t = ((ft < (float)MAXIMUM_CAPACITY) ?
	                 (int)ft : MAXIMUM_CAPACITY);
	        // 得到要創建的初始容量,爲2的冪次個,存入threshold
	        if (t > threshold)
	            threshold = tableSizeFor(t);
	    }
	    // 當初始化過後,則重新調整容量大小
	    else if (s > threshold)
	        resize();
	    // 遍歷map中的元素
	    for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
	        K key = e.getKey();
	        V value = e.getValue();
	        // 調用函數,將hash值key,key,value存入到桶中
	        putVal(hash(key), key, value, false, evict);
	    }
	}
}

其中調用了resize()方法和putVal()方法和hash()方法,源碼如下:

final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
    // tab爲放節點的桶,p爲節點,他們只是被封裝成了Node結構
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    // 如果桶是新桶沒有內容或者桶長度爲空,則調用resize得到需要的槽,resize操作如下---
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    // (n -1) & hash表示得到key經過hash映射後的索引,p = tab[i]表示指向key要放的位置
    // 當p爲空即第一次放時候,調用newNode創建一個頭節點
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    // 如果要放的位置已經有元素了,分兩種情況
    // 1. key值相同,則替換val
    // 2. key值不同,則存於鏈表中或者紅黑樹中
    else {
        Node<K,V> e; K k;
        // 情況1,如果要放的key的hash值相等(衝突)且 (key相同或(key不爲空且equal判等))
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            // 則p指向該節點
            e = p;
        // 情況2,如果現在p節點爲紅黑樹結構,則放入到紅黑樹中
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        // 否則放入到鏈表中
        else {
            // 遍歷鏈表
            for (int binCount = 0; ; ++binCount) {
                // 如果遍歷到最後一個節點,即沒有找到key值相同的,則生成一個新節點
                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;
                }
                // 如果遍歷中途發現key的hash值映射相同,且要放置的key與映射位置的key相同或(key不爲空且equal判等)
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                // 則p指向該節點
                p = e;
            }
        }
        // 如果映射的key指針e不爲空,則進行替換值
        if (e != null) { // existing mapping for key
            // 將舊值保存
            V oldValue = e.value;
            // 用新值替換舊值
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            // 
            afterNodeAccess(e);
            return oldValue;
        }
    }
    // modeCount用來記錄是否變化,hashmap使用fast-fail迭代機制,如果在迭代過程中發現modecount是否爲exceptedmodecount,如果不是則返回異常
    ++modCount;
    // 如果容量大於門限,則調整容量
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

hash()方法源碼如下:

static final int hash(Object key) {
    int h;
    // hashCode()調用的是頂層Object類的native函數 "public native int hashCode();",目的是爲了得到key的hashCode值
    // hashmap允許放key爲null的值,當爲null,則表示位於桶0號位置,爲什麼會用異或一個無符號右移的16位置自己?
    // 這樣做的目的是爲了讓hash更隨機,高位右移與高位異或,高位保留了信息,低位也保留了高位的信息,隨機性大
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

resize()方法源碼如下:

final Node<K,V>[] resize() {
    // oldTab保存舊的table
    Node<K,V>[] oldTab = table;
    // 得到舊的oldTab的容量,門限值
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    // 新的table容量,門限值
    int newCap, newThr = 0;
    // 當原來的表不是空的,即有值
    if (oldCap > 0) {
        // 如果舊的桶大於最大容量,則直接讓門限值爲最大整型值,這樣以後就不用自動擴容了
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        // 新容量爲舊的 * 2,且小於最大容量,且舊容量大於默認初始容量
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            // 前面定義的新門限也翻倍
            newThr = oldThr << 1; // double threshold
    }
    // 如果之前的oldCap <= 0,表示新創建的HashMap,如果oldThr > 0,表示調用HashMap構造函數HashMap(int initialCapacity)成功並初始化了
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;
    // 如果之前的oldCap<= 0 且oldThr < 0 表示調用HashMap空參構造函數HashMap()成功
    else {               // zero initial threshold signifies using defaults
        // 使用默認值初始化
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    // 如果新的門限值爲0,對於新創建的HashMap需要計算它的應該門限值
    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"})
    // 初始化table
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
    // 如果舊錶不爲空
    if (oldTab != null) {
        遍歷舊錶
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            // 得到舊錶桶的每個放置桶,如果不爲空
            if ((e = oldTab[j]) != null) {
                // 舊置空
                oldTab[j] = null;
                // 如果該節點後面沒有掛載其他節點,即單個節點,重新定位放置,否則判斷
                if (e.next == null)
                    newTab[e.hash & (newCap - 1)] = e;
                // 如果後面還有,且爲紅黑樹節點,則進行紅黑樹的重新hash映射找位置放
                else if (e instanceof TreeNode)
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                // 如果後面還有,且爲鏈表節點,則重新hash映射後鏈起來
                else { // preserve order
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    do {
                        next = e.next;
                        // 如果當前桶指向的元素與舊元素hash值相同,說明重新hash後位置不變
                        if ((e.hash & oldCap) == 0) {
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        // 否則爲變化的鏈表
                        else {
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    // 後面就是一些鏈表操作處理了
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

resize()結構圖如下:傳送門
在這裏插入圖片描述

3. 大體流程結構圖(參考原文:傳送門)

在這裏插入圖片描述

4. 總結

比較新手,也參考了一些文章。建議看源碼不舒服的,使用debug一步步走下去,然後看着註釋理解就ok,不懂就參考幾個一起看,理解就會更深刻。看加整理耗費了好幾天,如果有疏漏,懇請指正。

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