面試必備1:HashMap(JDK1.8)原理以及源碼分析

首先附上我的幾篇其它文章鏈接感興趣的可以看看,如果文章有異議的地方歡迎指出,共同進步,順便點贊謝謝!!!
Android framework 源碼分析之Activity啓動流程(android 8.0)
Android studio編寫第一個NDK工程的過程詳解(附Demo下載地址)
面試必備2:JDK1.8LinkedHashMap實現原理及源碼分析
Android事件分發機制原理及源碼分析
View事件的滑動衝突以及解決方案
Handler機制一篇文章深入分析Handler、Message、MessageQueue、Looper流程和源碼
Android三級緩存原理及用LruCache、DiskLruCache實現一個三級緩存的ImageLoader

HashMap概述:

在分析HashMap的源碼前,需要了解一下幾個問題。
面試必備2LinkedHashMap的源碼分析(基於JDK1.8)

1:HashMap的數據結構

我們需要先知道HashMap是基於什麼樣的數據結構來進行數據存儲的,知道了這些我們再去看源碼就容易的多。

散列表(哈希表) 我們常用的數據結構就是數組和鏈表,數組具有增刪慢查找快的特點,而鏈表具有增刪快查找慢 的特點;基於上述特點,HashMap 即想要查詢效率快,又想增刪效率高,基於這樣的特點HashMap的數據結構就是通過數組和鏈表組成的散列鏈表。
一直到JDK7爲止,HashMap的結構都基於一個數組以及多個鏈表的實現,hash值衝突的時候,就將對應節點以鏈表的形式存儲;JDK8中,HashMap採用的是數組(位桶)+鏈表/紅黑樹組成,當同一個hash值的節點數不小於8時,將不再以單鏈表的形式存儲了,會被調整成一顆紅黑樹,提高查詢效率,這就是JDK7與JDK8中HashMap實現的最大區別。

在這裏插入圖片描述

2:存儲節點 : Node 和TreeNode

HashMap存儲數據時如何組織出這樣一個散列表呢?HashMap中將我們存入的每一個數據封裝成一個Node類(Node節點),Node應該有以下下屬性 :key--------鍵、value-----值 組成我們put的鍵值對,既然要組織起鏈表則必須有Node next屬性、還有一個根據key算出的int 類型hash值,hash值用來確定該該節點所在數組的索引後面會進行詳細描述。

/**
 *  散列表的節點  Node  部分源碼
 */
 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;
        }
		/**
         *  返回當前節點的根節點  
         */
        final TreeNode<K,V> root() {
            for (TreeNode<K,V> r = this, p;;) {
                if ((p = r.parent) == null)
                    return r;
                r = p;
            }
        }
    }

當鏈表長度大於8時將鏈表轉換成紅黑樹,紅黑樹節點TreeNode部分源碼

 static final class TreeNode<K,V> extends LinkedHashMap.LinkedHashMapEntry<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);
        }
}

當我們放入或取出元素時大致流程是:

  1. 現根據hash函數去求出key的Hash值,至於求hash值得算法將在後面進行詳細分析
  2. 根據上一步的hash值可以確定在數組的哪個位置,定下標(數組的索引)
  3. 根據索引從數組中獲取數據,如果爲NULL 則將該元素直接放入或取出(直接在數組中操作),如果該位置數據不爲NuLL,則需要向該元素所在的鏈表進行數據的操作。

哈希衝突(哈希碰撞)

如果兩個不同的元素,通過哈希函數得出的實際存儲地址相同怎麼辦?也就是說,當我們對某個元素進行哈希運算,得到一個存儲地址,然後要進行插入的時候,發現已經被其他元素佔用了,其實這就是所謂的哈希衝突,也叫哈希碰撞。好的哈希函數會盡可能地保證 計算簡單和散列地址分佈均勻,但是,我們需要清楚的是,數組是一塊連續的固定長度的內存空間,再好的哈希函數也不能保證得到的存儲地址絕對不發生衝突。哈希衝突的解決方案有多種:開放定址法(發生衝突,繼續尋找下一塊未被佔用的存儲地址),再散列函數法,鏈地址法,而HashMap即是採用了鏈地址法,也就是數組+鏈表的方式。

3:HashMap中散列表中數組(或位桶)如何表示?

HashMap源碼中與數組相關的幾個成員屬性:

  1. 數組的表示: transient Node<K,V>[] table;
  2. 數組的大小 : 數組的初始化需要一個指定的大小,在HashMap中 數組的大小永遠是 2的冪次方(原因將在源碼中分析)
  3. 數組的默認大小:static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 , 1 << 4 二進制中1左移四位就是2^4=16, 不直接寫16的原因是,效率高,計算機最終讀取的二進制;
  4. 數組的最大容量:static final int MAXIMUM_CAPACITY = 1 << 30//最大爲2^30,使用位移操作符的原因也是效率高 ;
  5. 數組擴容因子: static final float DEFAULT_LOAD_FACTOR = 0.75f//用於確定數組擴容的臨界值;
  6. 以存放數據大小:transient int size//記錄 hashMap 當前存儲的元素的數量;
  7. 數組擴容的臨界值:int threshold;//數組已使用量size>threshold時需要擴容,threshold=數組大小*擴容因子提升效率,沒有必要等到數組都用完了在進行擴容,每次擴大2倍
    **注意:數組的擴容是在當已使用量大於數組容量*擴容因子(默認0.75)**時進行擴容

4:HashMap中散列表中鏈表的長度限制:長度越長存儲查詢效率低的問題

鏈表越長,期查詢效率越低,所以鏈表的長度也有一個限制,我們稱爲閾值 ,JDK1.8的時候,鏈表的長度大於閾值,將其結構改成一個紅黑二叉樹,以提升差值效率,同時也伴隨者性能的損耗增加
HashMap源碼中與鏈表相關的幾個成員屬性:

  1. 轉換紅黑樹的閾值: static final int TREEIFY_THRESHOLD = 8;//閾值 鏈表越深查找存儲效率都低 ,超過8以後將其轉換成紅黑二叉樹 ,提升查找效率
  2. 轉換成鏈表的閾值static final int UNTREEIFY_THRESHOLD = 6; //當某個桶節點數量小於6時,會轉換爲鏈表,前提是它當前是紅黑樹結構。
  3. static final int MIN_TREEIFY_CAPACITY = 64;//當整個hashMap中元素數量大於64時,也會進行轉爲紅黑樹結構。

6:新的數據封裝成的Node對象如何去存儲:
根據key計算hashCode ----》int類型的值,hash(key)然後就知道要存在數組的那個位置

5:HashMap中的屬性的概述

這裏將對HashMap屬性進行描述,以便於理解源碼

 public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {
        //序列號,序列化的時候使用。
        private static final long serialVersionUID = 362498820763181265L;
        /**默認容量,1向左移位4個,00000001變成00010000,也就是2的4次方爲16,使用移位是因爲移位是計算機基礎運算,效率比加減乘除快。**/
        static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
        //最大容量,2的30次方。
        static final int MAXIMUM_CAPACITY = 1 << 30;
        //加載因子,用於擴容使用。
        static final float DEFAULT_LOAD_FACTOR = 0.75f;
        //當某個桶節點數量大於8時,會轉換爲紅黑樹。
        static final int TREEIFY_THRESHOLD = 8;
        //當某個桶節點數量小於6時,會轉換爲鏈表,前提是它當前是紅黑樹結構。
        static final int UNTREEIFY_THRESHOLD = 6;
        //當整個hashMap中元素數量大於64時,也會進行轉爲紅黑樹結構。
        static final int MIN_TREEIFY_CAPACITY = 64;
        //存儲元素的數組,transient關鍵字表示該屬性不能被序列化
        transient Node<K,V>[] table;
        //將數據轉換成set的另一種存儲形式,這個變量主要用於迭代功能。
        transient Set<Map.Entry<K,V>> entrySet;
        //元素數量
        transient int size;
        //統計該map修改的次數
        transient int modCount;
        //臨界值,也就是元素數量達到臨界值時,會進行擴容。
        int threshold;
        //也是加載因子,只不過這個是變量。
        final float loadFactor;
        ....
    }

HashMap源碼分析:

瞭解了概述中的幾個概念後,將從對HashMap的put、get、remove三個方法進行源碼分析。

1:HashMap的put(key,value)方法源碼

put方法的大致流程:

  1. 根據傳入的key,計算hash值
  2. 判斷鍵值對數組tab[]是否爲空或爲null,否則以默認大小resize()進行數組的初始化操作;
  3. 根據鍵值key計算hash值得到插入的數組索引i,如果tab[i]==null,直接新建節點添加,否則轉入3
  4. 判斷當前數組中處理hash衝突的方式爲鏈表還是紅黑樹(check第一個節點類型即可),分別處理
//返回值扔然爲Value
public V put(K key, V value) {
		//1:hash(key)先找存在哪裏,先算key的hash值
		//2: putVal(hash(key), key, value, false, true)方法進行存儲
        return putVal(hash(key), key, value, false, true);
    }

//根據key求出Hash值,通過這去求散列表的下標
static final int hash(Object key) {
        int h;//32位數,不足高位補0 
        // (h = key.hashCode()) ^ (h >>> 16)  
       // 1: key的HashCode     h  
        //2:  h先做了個位移運算,向右邊位移16位即h >>> 16  
       /// 3:然後將兩個值進行異或預算    充分利用hash的每一位數,將h的高16位異或h的低16位得到一個值, 使得高位也可以參與hash,更大程度上減少了碰撞率。
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

putVal方法源碼:

//,如果參數onlyIfAbsent是true,那麼不會覆蓋相同key的值value。如果evict是false。那麼表示是在初始化時調用的
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)//數組table爲null
             n = (tab = resize()).length;//resize()  作用1:初始化數組   n記錄數組的大小
        if ((p = tab[i = (n - 1) & hash]) == null) // tab[i = (n - 1) & hash]==null, 即數組tab[i]==null,沒有元素直接新建節點添加
     /**
     *  根據hash和數組長度算出數組下標i  table[i]取出對應元素
     *  i = (n - 1) & hash 算出數組下標的優點?
     *  1:防止數組越界:這麼寫是爲了解決hash有可能越界問題  ,即hash不能大於n 數組長度,
     *     數組的大小n一定是2幾次冪,只有這樣n-1,n-1二進制表示時每位的值都爲1,從而保證在(n-1)&hash值的值永遠<=n-1 ,保證不會數組越界
     *     例如:當n=16默認值時,n - 1=15 的二進制 01111 和前面的hash值進行與運算   的值永遠小於等於01111(15),不會越界
     *  2:提高效率:(n - 1) & hash等價於  模/n取餘 這樣寫是因爲與運算比模運算效率高
     *  3: 數組的大小n一定是2幾次冪,n-1 的二進制形式每位都是1,在&hash時導致數組的散列性變大,降低了hash碰撞的概率
     */
            tab[i] = newNode(hash, key, value, null);
        else {//tab[i]!=null需要向鏈表或紅黑樹中存放
            Node<K,V> e; K k;
            if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))
			// 如果這個元素的key與要插入的一樣,那麼就替換
                e = p;
            else if (p instanceof TreeNode)
            	//1.如果當前節點是TreeNode類型的數據,執行putTreeVal方法, 向紅黑樹中存放
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
            	 //遍歷這條鏈子上的數據,跟jdk7沒什麼區別
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {//即table[i].next==null,table[i]就是尾節點
                       //向鏈表尾部添加數據
                        p.next = newNode(hash, key, value, null);
                        //項鍊表中添加完元素後判斷,鏈表長度是否超過8
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        	//長度超過8,執行treeifyBin方法,將鏈表轉換成紅黑樹
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
             // 表示在桶中找到key值、hash值與插入元素相等的結點
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                // 返回舊值
                return oldValue;
            }
        }
        ++modCount;
         //每次添加完成後,判斷閾值,決定是否擴容
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);//這個爲子類HashMap服務,進行lru排序,在這裏無需研究
        return null;
    }

treeifyBin方法源碼:將鏈表轉換成紅黑二叉樹

final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; Node<K,V> e;
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            resize();
        else if ((e = tab[index = (n - 1) & hash]) != null) {
            TreeNode<K,V> hd = null, tl = null;
            do {
                TreeNode<K,V> p = replacementTreeNode(e, null);//將Node轉成TreeNode
                if (tl == null)
                    hd = p;
                else {
                    p.prev = tl;
                    tl.next = p;
                }
                tl = p;
            } while ((e = e.next) != null);
            if ((tab[index] = hd) != null)
                hd.treeify(tab);
        }
    }

2:HashMap的擴容機制:resize()源碼

resize的作用:1:初始化數組table 2: 擴容
構造hash表時,如果不指明初始大小,默認大小爲16(即Node數組大小16),如果Node[]數組中的元素達到(填充比*Node.length)重新調整HashMap大小 變爲原來2倍大小,擴容很耗時

  1. 每次擴展的時候,都是擴展2倍;
  2. 擴展後Node對象的位置要麼在原位置,要麼移動到原偏移量兩倍的位置。
//resize  作用1:初始化數組   2擴容
final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;//記錄數組的長度
        int oldThr = threshold;
        int newCap, newThr = 0;
        if (oldCap > 0) {//數組大於0進行擴容操作
            if (oldCap >= MAXIMUM_CAPACITY) {//如果容量已大於最大值
                threshold = Integer.MAX_VALUE;//修改擴容臨界值
                return oldTab;
            }
            //如果oldCap << 1擴大兩倍,滿足擴容的條件
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // 左移1位擴大兩倍容量
        }
         // 如果舊錶的長度的是0,就是說第一次初始化表
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        else {               // zero initial threshold signifies using defaults
        	//數組的初始化
            newCap = DEFAULT_INITIAL_CAPACITY;//默認大小
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);//
        }
        if (newThr == 0) {//新表長度乘以加載因子  
            float ft = (float)newCap * loadFactor;//新的數組容量
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr;//threshold   容量*負載因子    =臨界值
        @SuppressWarnings({"rawtypes","unchecked"})
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];//初始化數組   一開始默認到校
       table = newTab;//把新表賦值給table  
       if (oldTab != null) {//原表不是空要把原表中數據移動到新表中
        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)//說明這個node沒有鏈表直接放在新表的數組e.hash & (newCap - 1)位置  
                       newTab[e.hash & (newCap - 1)] = e;  
                    else if (e instanceof TreeNode)
                    	//如果是紅黑樹則,轉爲鏈表
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { // preserve order
                       //如果e後邊有鏈表,到這裏表示e後面帶着個單鏈表,需要遍歷單鏈表,將每個結點重新計算在新表的位置,並進行搬運  
                        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),可以得到哈希值去模後,
          					 //是大於等於oldCap還是小於oldCap,等於0代表小於oldCap,應該存放在低位,
          					 //否則存放在高位。這裏又是一個利用位運算 代替常規運算的高效點 
                            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) {//lo隊不爲null,放在新表原位置  
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {//hi隊不爲null,放在新表j+oldCap位置  
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;//返回初始化或者擴容後的數組
    }

3:小結:

  • 運算儘量都用位運算代替,更高效。
  • 對於擴容導致需要新建數組存放更多元素時,除了要將老數組中的元素遷移過來,也記得將老數組中的引用置null,以便GC
  • 取下標 是用 哈希值 與運算 (桶的長度-1) i = (n - 1) & hash。 由於桶的長度是2的n次方,這麼做其實是等於 一個模運算。但是效率更高
  • 擴容時,如果發生過哈希碰撞,節點數小於8個。則要根據鏈表上每個節點的哈希值,依次放入新哈希桶對應下標位置。
  • 因爲擴容是容量翻倍,所以原鏈表上的每個節點,現在可能存放在原來的下標,即low位, 或者擴容後的下標,即high位。 high位= low位+原哈希桶容量
  • 利用哈希值 與運算 舊的容量 ,if ((e.hash & oldCap) == 0),可以得到哈希值去模後,是大於等於oldCap還是小於oldCap,等於0代表小於oldCap,應該存放在低位,否則存放在高位。這裏又是一個利用位運算 代替常規運算的高效點
  • 如果追加節點後,鏈表數量》=8,則轉化爲紅黑樹
  • 插入節點操作時,有一些空實現的函數,用作LinkedHashMap重寫使用。

4:HashMap的get(key)方法源碼分析:

下面簡單說下 get(key) 的過程:
get(key)方法時獲取key的hash值,計算hash&(n-1)得到在鏈表數組中的位置first=tab[hash&(n-1)],先判斷first的key是否與參數key相等,不等就遍歷後面的鏈表找到相同的key值返回對應的Value值即可,即先根據hash值確定數組(哈希桶)位置,然後根據key相等取值 , hash相等&&key相等的節點

//傳入鍵  key
public V get(Object key) {
        Node<K,V> e;
        //根據hash函數得到的hash值和key去 getNode(hash(key), key)  返回null或者Node.value
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }

getNode(hash, key)方法源碼分析:

//根據 hash  和key獲取node節點
final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;//在tab數組中經過散列的第一個位置  
           // table已經初始化,長度大於0,根據hash尋找table中的項也不爲空
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            if (first.hash == hash &&// 判斷是不是第一個節點,是的話從數組中返回
                ((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;
    }

5:remove方法源碼分析

public V remove(Object key) {
        Node<K,V> e;
         //這裏傳入了value 同時matchValue爲true
        return (e = removeNode(hash(key), key, null, false, true)) == null ?null : e.value;
    }

removeNode(int hash, Object key, Object value,boolean matchValue, boolean movable)源碼

 /**
     * Implements Map.remove and related methods
     * @param hash 哈希值
     * @param key the key  
     * @param value the value to match if matchValue, else ignored
     * @param matchValue 爲true的話,則表示刪除它key對應的value,不刪除key
     * @param movable 如果爲false,則表示刪除後,不移動節點,爲true則移動節點
     * @return the node, or null if none
     */
   final Node<K,V> removeNode(int hash, Object key, Object value,
                               boolean matchValue, boolean movable) {
        //tab 哈希數組,p 數組下標的節點,n 長度,index 當前數組下標
        Node<K,V>[] tab; Node<K,V> p; int n, index;
        //哈希數組不爲null,且長度大於0,然後獲得到要刪除key的節點所在是數組下標位置
        if ((tab = table) != null && (n = tab.length) > 0 &&
                (p = tab[index = (n - 1) & hash]) != null) {
            //node 存儲要刪除的節點,e 臨時變量,k 當前節點的key,v 當前節點的value
            Node<K,V> node = null, e; K k; V v;
            //如果數組下標的節點正好是要刪除的節點,把值賦給臨時變量node
            if (p.hash == hash &&
                    ((k = p.key) == key || (key != null && key.equals(k))))
                node = p;
                //也就是要刪除的節點,在鏈表或者紅黑樹上,先判斷是否爲紅黑樹的節點
            else if ((e = p.next) != null) {
                if (p instanceof TreeNode)
                    //遍歷紅黑樹,找到該節點並返回
                    node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
                else { //表示爲鏈表節點,一樣的遍歷找到該節點
                    do {
                        if (e.hash == hash &&
                                ((k = e.key) == key ||
                                        (key != null && key.equals(k)))) {
                            node = e;
                            break;
                        }
                        /**注意,如果進入了鏈表中的遍歷,那麼此處的p不再是數組下標的節點,而是要刪除結點的上一個結點**/
                        p = e;
                    } while ((e = e.next) != null);
                }
            }
            //找到要刪除的節點後,判斷!matchValue,我們正常的remove刪除,!matchValue都爲true
            if (node != null && (!matchValue || (v = node.value) == value ||
                    (value != null && value.equals(v)))) {
                //如果刪除的節點是紅黑樹結構,則去紅黑樹中刪除
                if (node instanceof TreeNode)
                    ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
                    //如果是鏈表結構,且刪除的節點爲數組下標節點,也就是頭結點,直接讓下一個作爲頭
                else if (node == p)
                    tab[index] = node.next;
                else /**爲鏈表結構,刪除的節點在鏈表中,把要刪除的下一個結點設爲上一個結點的下一個節點**/
                    p.next = node.next;
                //修改計數器
                ++modCount;
                //長度減一
                --size;
                /**此方法在hashMap中是爲了讓子類去實現,主要是對刪除結點後的鏈表關係進行處理**/
                afterNodeRemoval(node);
                //返回刪除的節點
                return node;
            }
        }
        //返回null則表示沒有該節點,刪除失敗
        return null;
    }

HashMap的遍歷entrySet()

entrySet()源碼:

 public Set<Map.Entry<K,V>> entrySet() {
        Set<Map.Entry<K,V>> es;
        //直接返回成員變量entrySet==null時 new  EntrySet()並返回,否則直接返回
        return (es = entrySet) == null ? (entrySet = new EntrySet()) : es;
    }

集合的遍歷主要是通過內部類EntrySetiterator()方法實現,EntrySet的部分源碼如下:


final class EntrySet extends AbstractSet<Map.Entry<K,V>> {
        public final int size()                 { return size; }
        public final void clear()               { HashMap.this.clear(); }
        public final Iterator<Map.Entry<K,V>> iterator() {
        	//返回EntryIterator類型的迭代器
            return new EntryIterator();
        }
   		 ....省略部分源碼...
    }

iterator方法返回EntryIterator類型的迭代器其源碼如下

final class EntryIterator extends HashIterator
        implements Iterator<Map.Entry<K,V>> {
        //Iterator的`next`方法就是調用父類`HashIterator`的`nextNode`方法獲取下一個節點
        public final Map.Entry<K,V> next() { return nextNode(); }
    }

nextNode方法是父類HashIterator提供的方法,即HaapMap的迭代的核心就是HashIterator源碼的源碼如下,這裏是重點!!!

abstract class HashIterator {
        Node<K,V> next;        // 下一個節點
        Node<K,V> current;     // 當前節點
        int expectedModCount;  // HashMap是線程不安全的,在迭代時通過expectedModCount去記錄成員變量modCount,判斷迭代過程中的併發修改異常
        int index;             // 當前數組的索引
		/**
		  * 構造器中對屬性進行了初始化,
		  * 並通過while循環得到數組中的第一個不爲空的元素下標以及值,並將此元素值賦給next
		  */
        HashIterator() {
            expectedModCount = modCount;
            Node<K,V>[] t = table;
            current = next = null;
            index = 0;
            //while循環得到數組中的第一個不爲空的元素下標以及值,並將此元素值賦給next
            if (t != null && size > 0) { // advance to first entry
                do {} while (index < t.length && (next = t[index++]) == null);
            }
        }
        //判斷是否有下一個節點
        public final boolean hasNext() {
            return next != null;
        }
		/**
          * nextNode()的方法,就是Iterator中next方法中調用的,即Iterator中next方法就是nextNode方法
         * 其遍歷HashMap過程就是,就是依次遍歷數組中的鏈表,取出下一個節點
        */
        final Node<K,V> nextNode() {
            Node<K,V>[] t;
            Node<K,V> e = next;//e,爲當前要返回的節點
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            if (e == null)
                throw new NoSuchElementException();
                //HashMap遍歷的核心思想
             //e爲當前要返回的節點,
             //獲取e的下一節點,並賦值給next,如果此時的next爲空,則說明當前鏈表已經遍歷完畢,
             //那麼進入判斷體,開始通過while遍歷下一個數組桶中的鏈表,並將此鏈表的頭結點賦值給next;
            if ((next = (current = e).next) == null && (t = table) != null) {
                do {} while (index < t.length && (next = t[index++]) == null);
            }
            return e;
        }

		 /**
           * Iterator的remove刪除方法 本質上還是調用了HashMap的removeNode方法
          * 只是在調用之前,通過modCount != expectedModCount時拋出併發修改異常,處理線程不安全問題,
          * 如果相等則調用HashMap的removeNode方法移除節點
          * 
        */
        public final void remove() {
            Node<K,V> p = current;
            if (p == null)
                throw new IllegalStateException();
            if (modCount != expectedModCount)//處理多線程併發問題
                throw new ConcurrentModificationException();
            current = null;
            K key = p.key;
            removeNode(hash(key), key, null, false, false);
            expectedModCount = modCount;
        }
    }

HashMap的遍歷過程就是通過nextNode取出下一個節點的過程,它的核心是想就是依次遍歷哈希桶中的鏈表實現。

總結

到此爲止,HashMap原理分析完畢,閱讀時需要從第一部分先看,然後再看第二部分的源碼分析,具體的總結在概述和小結中都已經描述清楚,這裏不做過多贅述。這是週末看的HashMap源碼的理解,裏面有不對的對方歡迎大家留言指處,共同進步,後續會陸續將LinkedHashMap、CurrentHashMap的源碼分享出來以供參考。

趁熱打鐵接下來可以看一下我的另一片文章:面試必備2LinkedHashMap的源碼分析(基於JDK1.8)

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