HashMap底層原理及jdk1.8源碼解讀

語言: CN / TW / HK

一、前言

寫在前面:小編碼字收集資料花了一天的時間整理出來,對你有幫助一鍵三連走一波哈,謝謝啦!!

HashMap在我們日常開發中可謂經常遇到,HashMap 源碼和底層原理在現在面試中是必問的。所以我們要掌握一下,也是作為兩年開發經驗必備的知識點!HashMap基於Map接口實現,元素以 <Key,Value> 的方式存儲,並且允許使用null 鍵和null值,因為key不允許重複,因此只能有一個鍵為null,另外HashMap是 無序的、線程不安全 的。

HashMap繼承類圖 快捷鍵Ctrl+alt+U

二、存儲結構介紹

Jdk7.0之前 數組 + 鏈表
Jdk8.0開始 數組 + 鏈表 + 二叉樹
鏈表內元素個數>8個 由鏈表轉成二叉樹
鏈表內元素個數<6個 由二叉樹轉成鏈表
紅黑樹,是一個自平衡的二叉搜索樹,因此可以使查詢的時間複雜度降為 O(logn)
鏈表的長度特別長的時候,查詢效率將直線下降,查詢的時間複雜度為 O(n)
Jdk1.7中鏈表新元素添加到鏈表的頭結點,先加到鏈表的頭節點,再移到數組下標位置。簡稱 頭插法(併發時可能出現死循環)
Jdk1.8中鏈表新元素添加到鏈表的尾結點。簡稱 尾插法(解決了頭插法死循環)

底層結構實例圖

三、源碼分析之常用變量解讀

/**
 * 默認初始容量 - 必須是 2 的冪。
 */
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

/**
 * 最大容量,如果一個更高的值被任何一個帶參數的構造函數隱式指定使用。必須是 2 <= 1<<30 的冪。
 * 簡單理解為:最大容量2的30次冪,如果帶容量也會轉化為2的次冪
 */
static final int MAXIMUM_CAPACITY = 1 << 30;

/**
 * 構造函數中未指定時使用的負載因子。經過官方測試測出減少hash碰撞的最佳值
 */
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;

/**
 * 在首次使用時初始化,並根據需要調整大小。分配時,長度始終是 2 的冪。
 * (我們還在某些操作中允許長度為零,以允許當前不需要的引導機制)
 */
transient Node<K,V>[] table;

/**
 * 保存緩存的 entrySet(),也就是存放的鍵值對
 */
transient Set<Map.Entry<K,V>> entrySet;

/**
 * 此映射中包含的鍵值映射的數量,就是數組的大小
 */
transient int size;

/**
 * 記錄集合的結構變化和操作次數(例如remove一次進行modCount++)。
 * 該字段用於使 HashMap 的 Collection-views 上的迭代器快速失敗。如果失敗也就是我們常見的CME
 * (ConcurrentModificationException)異常
 */
transient int modCount;

/**
 * 數組擴容的大小
 * 計算方法 capacity * load factor  容量 * 加載因子
 * @serial
 */
int threshold;

/**
 * 哈希表的負載因子。
 * @serial
 */
final float loadFactor;

四、源碼分析之構造方法解讀

/**
 * 構造一個具有指定初始容量和負載因子的構造方法
 * 不建議使用這個構造方法,加載因子經過官方測試是效率最好的,所以為了效率應該保持默認
 */
public HashMap(int initialCapacity, float loadFactor) {
	// 參數為負數會拋出IllegalArgumentException異常
    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;
    // 初始化的參數默認如果不是2的次冪,直接給你轉化為2的次冪
    // 傳的參數為11,會自動轉化為比參數大的最近的2的次冪的值,也就是16。
    // 我們後面會詳細説這個方法怎麼進行轉化的
    this.threshold = tableSizeFor(initialCapacity);
}

/**
 * 構造一個只有指定初始容量的方法
 */
public HashMap(int initialCapacity) {
    // 我們會發現還是調用上面兩個參數的構造方法,自動幫我們設置了加載因子
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

/**
 * 構造一個具有默認初始容量(16) 和默認負載因子(0.75)的構造方法
 */
public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

/**
 * 指定map集合,轉化為HashMap的構造方法
 */
public HashMap(Map<? extends K, ? extends V> m) {
	// 使用默認加載因子
    this.loadFactor = DEFAULT_LOAD_FACTOR;
	//調用PutMapEntries()來完成HashMap的初始化賦值過程
    putMapEntries(m, false);
}
/**
 * 將傳入的子Map中的全部元素逐個添加到HashMap中
 */
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
	// 獲得參數Map的大小,並賦值給s
    int s = m.size();
    // 判斷大小是否大於0
    if (s > 0) {
    	// 證明有元素來插入HashMap
    	// 判斷table是否已經初始化  如果table=null一般就是構造函數來調用的putMapEntries,或者構造後還沒放過任何元素
        if (table == null) { // pre-size
        	// 如果未初始化,則計算HashMap的最小需要的容量(即容量剛好不大於擴容閾值)。這裏Map的大小s就被當作HashMap的擴容閾值,然後用傳入Map的大小除以負載因子就能得到對應的HashMap的容量大小(當前m的大小 / 負載因子 = HashMap容量)
            // ((float)s / loadFactor)但這樣會算出小數來,但作為容量就必須向上取整,所以這裏要加1。此時ft可以臨時看作HashMap容量大小
            float ft = ((float)s / loadFactor) + 1.0F;
            // 判斷ft是否超過最大容量大小,小於則用ft,大於則取最大容量
            int t = ((ft < (float)MAXIMUM_CAPACITY) ?
                     (int)ft : MAXIMUM_CAPACITY);
            // 暫時存放到擴容閾值上,tableSizeFor會把t重新計算到2的次冪給擴容數組大小
            if (t > threshold)
                threshold = tableSizeFor(t);
        }
        // 如果當前Map已經初始化,且這個map中的元素個數大於擴容的閥值就得擴容
        // 這種情況屬於預先擴大容量,再put元素
        else if (s > threshold)
        	// 後面展開説
            resize();
        // 遍歷map,將map中的key和value都添加到HashMap中
        for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
            K key = e.getKey();
            V value = e.getValue();
            // 調用HashMap的put方法的具體實現方法putVal來對數據進行存放。該方法的具體細節在後面會進行講解
            // putVal可能也會觸發resize
            putVal(hash(key), key, value, false, evict);
        }
    }
}

五、源碼分析之常用方法解讀

1、tableSizeFor方法解讀

/**
 * 返回給定目標容量的 2 次方。
 */
static final int tableSizeFor(int cap) {
	// cap = 10
    int n = cap - 1;
    n |= n >>> 1;
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
    // 小於0就是1,如果大於0在判斷是不是超過最大容量就是n=15+1 = 16,超過就按最大容量
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

cap為10 為例:(右移規則為:無符號位右移)

10的二進制為: 0000 0000 0000 0000 0000 0000 0000 1010

執行 int n = cap - 1;

二進制結果為: 0000 0000 0000 0000 0000 0000 0000 1001

執行 n |= n >>> 1;(先進行右移,結果和原來的數進行或運算[==有1則1==])

0000 0000 0000 0000 0000 0000 0000 1001

0000 0000 0000 0000 0000 0000 0000 0100 n>>1結果

————————————————————

0000 0000 0000 0000 0000 0000 0000 1101 n |= n >> 1 結果

二進制結果為: 0000 0000 0000 0000 0000 0000 0000 1101

執行 n |= n >>> 2;

0000 0000 0000 0000 0000 0000 0000 1101

0000 0000 0000 0000 0000 0000 0000 0011 n>>2結果

————————————————————

0000 0000 0000 0000 0000 0000 0000 1111 n |= n >> 2 結果

二進制結果為: 0000 0000 0000 0000 0000 0000 0000 1111

執行 n |= n >>> 4;

0000 0000 0000 0000 0000 0000 0000 1111

0000 0000 0000 0000 0000 0000 0000 0000 n>>4結果

————————————————————

0000 0000 0000 0000 0000 0000 0000 1111 n |= n >> 4 結果

二進制結果為: 0000 0000 0000 0000 0000 0000 0000 1111

執行 n |= n >>> 8;

0000 0000 0000 0000 0000 0000 0000 1111

0000 0000 0000 0000 0000 0000 0000 0000 n>>8結果

————————————————————

0000 0000 0000 0000 0000 0000 0000 1111 n |= n >> 8 結果

二進制結果為: 0000 0000 0000 0000 0000 0000 0000 1111

執行 n |= n >>> 16;

0000 0000 0000 0000 0000 0000 0000 1111

0000 0000 0000 0000 0000 0000 0000 0000 n>>16結果

————————————————————

0000 0000 0000 0000 0000 0000 0000 1111 n |= n >> 16 結果

二進制結果為: 0000 0000 0000 0000 0000 0000 0000 1111

我們會發現,當數小的的時候進行到4時就不會變了我們得到的值為15,即輸入10,經過此方法後得到 15 。遇到大的數才會明顯,大家可以找個大的數字進行試試就是先 右移 在進行 或運算 。最後進行三門運算進行 +1 操作,最後結果為 16=2^4

2、hash方法解讀

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

先判斷key是否為空,若為空則返回0。上面説了HashMap僅支持一個key為null的。若非空,則先計算key的hashCode值,賦值給h,然後把h右移16位,並與原來的h進行異或處理( 相同為1,不同為0 )。

註釋進行谷歌翻譯:

計算 key.hashCode() 並傳播(XOR)更高位的哈希降低。 由於該表使用二次冪掩碼,因此僅在當前掩碼之上位變化的散列集將始終發生衝突。 (已知的例子是在小表中保存連續整數的 Float 鍵集。)因此,我們應用了一種變換,將 高位的影響向下傳播 。 在位擴展的速度、實用性和質量之間存在折衷。 因為許多常見的散列集已經合理分佈(所以不要從傳播中受益),並且因為我們使用樹來處理 bin 中的大量衝突,我們只是以最簡單的方式對一些 移位的位進行異或 ,以 減少系統損失 , 以及 合併最高位的影響 ,否則由於表邊界,這些最高位將永遠不會用於索引計算。

總結: 使用最簡易的方式,及減少系統損失又減少了hash碰撞

3、put方法解讀

/**
* 將指定的值與此映射中的指定鍵相關聯。即當前key應該存放在數組的哪個下標位置
* 如果映射先前包含鍵的映射,則替換舊的值。
*/
public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}
/**
* 這才是真正的保存方法
* @param hash   			經過hash運算後的key
* @param key    			你要添加的key
* @param value  			你要添加的value
* @param onlyIfAbsent 		如果為真,則不要更改現有值,本次為FALSE,可以替換更改
* @param evict 				如果為 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是否為空
   if ((tab = table) == null || (n = tab.length) == 0)
   	   // 如果空的話,會先調用resize擴容,resize我們後面展開講解
       n = (tab = resize()).length;
   // 把通過hash得到的值與數組大小-1進行與運算,這個運算就可以實現取模運算,而且位運算還有個好處,就是速度比較快。
   // 得到key所對應的數組的節點,然後把該數組節點賦值給p,然後判斷這個位置是不是有元素
   if ((p = tab[i = (n - 1) & hash]) == null)
   	   // key、value包裝成newNode節點,直接添加到此位置。
       tab[i] = newNode(hash, key, value, null);
   // 如果當前數組下標位置已經有元素了,又分為三種情況。
   else {
       Node<K,V> e; K k;
       // 當前位置元素的hash值等於傳過來的hash,並且他們的key值也相等
       if (p.hash == hash &&
           ((k = p.key) == key || (key != null && key.equals(k))))
           // 則把p賦值給e,後續需要新值把舊值替換
           e = p;
       // 來到這裏説明該節點的key與原來的key不同,則看該節點是紅黑樹,還是鏈表
       else if (p instanceof TreeNode)
       	   // 如果是紅黑樹,則通過紅黑樹的方式,把key-value存到紅黑樹中。後面再講解這個方法
           e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
       else {
       	   // 來到這裏説明結構為鏈表,把key-value使用尾插法插到鏈表尾。
           // jdk1.7 鏈表是頭插入法,在併發擴容時會造成死循環
           // jdk1.8 就把頭插入法換成了尾插入法,雖然效率上有點稍微降低一些,但是不會出現死循環
           for (int binCount = 0; ; ++binCount) {
           	   // 遍歷該鏈表,知道知道下一個節點為null。
               if ((e = p.next) == null) {
               	   // 説明到鏈表尾部,然後把尾部的next指向新生成的對象
                   p.next = newNode(hash, key, value, null);
                   // 如果鏈表的長度大於等於8
                   if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                   	   // 則鏈表轉化成為紅黑樹 後面再補充
                       treeifyBin(tab, hash);
                   break;
               }
               // 如果在鏈表中找到了相同key的話,直接退出循環
               if (e.hash == hash &&
                   ((k = e.key) == key || (key != null && key.equals(k))))
                   break;
               p = e;
           }
       }
       // 説明發生了碰撞,e代表的是舊值,需要替換為新值
       if (e != null) { // existing mapping for key
           V oldValue = e.value;
           // 判斷是不是允許覆蓋舊值,和舊值是否為空
           if (!onlyIfAbsent || oldValue == null)
           		// 把舊值替換
               e.value = value;
           // hashmap沒有任何實現,我們先不考慮
           afterNodeAccess(e);
           // 返回新值
           return oldValue;
       }
   }
   // fail-fast機制每次對結構改變進行+1
   ++modCount;
   // 判斷HashMap中的存的數據大小,如果大於數組長度*0.75,就要進行擴容
   if (++size > threshold)
       resize();
   // 也是一個空的實現
   afterNodeInsertion(evict);
   return null;
}

4、resize方法解讀

看不清查看原鏈接: 高清圖鏈接

/**
 * 初始化或者是將table大小加倍,如果為空,則按threshold分配空間,
 * 否則加倍後,每個容器中的元素在新table中要麼呆在原索引處,要麼有一個2的次冪的位移
 */
final Node<K,V>[] resize() {
	// 原來的數據
    Node<K,V>[] oldTab = table;
    // 獲取原來的數組大小
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    // 舊數組的擴容閾值,注意看,這裏取的是當前對象的 threshold 值,下邊的第2種情況會用到。
    int oldThr = threshold;
    // 初始化新數組的容量和閾值
    int newCap, newThr = 0;
    // 1.當舊數組的容量大於0時,説明在這之前肯定調用過 resize擴容過一次,才會導致舊容量不為0。
    if (oldCap > 0) {
    	// 容量達到最大
        if (oldCap >= MAXIMUM_CAPACITY) {
        	// 擴容的大小設置為上限
            threshold = Integer.MAX_VALUE;
            // 直接返回默認原來的,無法在擴了
            return oldTab;
        }
        // 如果舊容量是在16到上限之間
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            // 新數組的容量和閾值都擴大原來的2倍
            newThr = oldThr << 1; // double threshold
    }
    // 2. 到這裏 oldCap <= 0, oldThr(threshold) > 0,這就是 map 初始化的時候,
    // 第一次調用 resize的情況
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;
    // 3. 到這裏,説明 oldCap 和 oldThr 都是小於等於0的。也説明我們的map是通過默認無參構造來創建的
    else {               // zero initial threshold signifies using defaults
        newCap = DEFAULT_INITIAL_CAPACITY;// 16
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);// 12
    }
    // 只有經過2.才會進入
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;
        // 把計算後的ft符合大小,則賦值newThr 
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    // 最後得到要擴容的大小
    threshold = newThr;
    // 用於抑制編譯器產生警告信息
    @SuppressWarnings({"rawtypes","unchecked"})
    	// 在構造函數時,並沒有創建數組,在第一次調用put方法,導致resize的時候,才會把數組創建出來。
    	// 這是為了延遲加載,提高效率。
        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;
                // 如果第一個元素的下一個元素為null,説明鏈表只有一個
                if (e.next == null)
                	// 則直接用它的hash值和新數組的容量取模就行(這樣運算效率高),得到新的下標位置
                    newTab[e.hash & (newCap - 1)] = e;
                // 如果是紅黑樹
                else if (e instanceof TreeNode)
                	// 則拆分紅黑樹,這個小編就不帶着往下深究了,感興趣可以自己點進去看看
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                // 説明是鏈表且長度大於1
                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值和oldCap做與運算為0,則原位置不變
                        if ((e.hash & oldCap) == 0) {
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        // 不為0則把數據移動到新位置
                        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;
}

5、get和containsKey方法解讀

/**
* 根據key獲取對應的value
*/
public V get(Object key) {
    Node<K,V> e;
    // 調用後存在就獲取元素的value返回
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}
/**
* 判斷key是否在map中存在
*/
public boolean containsKey(Object key) {
	// 調用方法存在及不為null
    return getNode(hash(key), key) != null;
}
/**
* 我們發現底層都是getNode來進行幹活的
*/
final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    // 判斷數組不能為空,然後取到當前hash值計算出來的下標位置的第一個元素
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {
        // 如果hash值和key都相等並不為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) {
        	// 如果是紅黑樹
            if (first instanceof TreeNode)
                // 則以紅黑樹的查找方式進行獲取到我們想要的key對應的值
                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);
        }
    }
    // 找不到key對應的值,返回null
    return null;
}

6、remove方法解讀

/**
 * 如果key存在,就把元素刪除
 */
public V remove(Object key) {
    Node<K,V> e;
    return (e = removeNode(hash(key), key, null, false, true)) == null ?
        null : e.value;
}

/**
 * 刪除方法的具體實現
* @param hash   			經過hash運算後的key
* @param key    			你要移除的key
* @param value  			你要移除的value
 * @param matchValue 		如果為真,則僅在值相等時刪除,我們為FALSE,key相同即刪除
 * @param movable 			如果為假,則在刪除時不要移動其他節點,我們為TRUE,刪除移動其他節點
 */
final Node<K,V> removeNode(int hash, Object key, Object value,
                           boolean matchValue, boolean movable) {
    Node<K,V>[] tab; Node<K,V> p; int n, index;
    // 判斷table不為空,鏈表不為空
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (p = tab[index = (n - 1) & hash]) != null) {
        Node<K,V> node = null, e; K k; V v;
        // 數組中的第一個節點就是我們要刪除的節點
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            // 要刪除的節點給node
            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
                        node = e;
                        break;
                    }
                    p = e;
                } while ((e = e.next) != null);
            }
        }
        // 這裏説明我們要刪除的節點已找到
        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;// p的下一個節點指向到node的下一個節點即可把node從鏈表中刪除了
            // 操作+1
            ++modCount;
            // map大小-1
            --size;
            // 空的實現
            afterNodeRemoval(node);
            // 返回刪除的節點
            return node;
        }
    }
    return null;
}

六、總結

Has和Map得底層還是很多的,裏面用的一些邏輯和算法,都是很牛皮的、耐人琢磨的。oracle裏的人才還是厲害啊,你看都看不明白,人家就能設計出來,實現出來。真的是你我皆為螻蟻,只不過是一個個搬磚工具人呀。哈哈,對自己的一個自嘲哈,共勉!!看到這裏了,點個贊吧,小編碼字實屬不易呀!!謝謝了!!

環境大家關注小編的微信公眾號!謝謝大家!

推廣自己網站時間到了!!!

點擊訪問!歡迎訪問,裏面也是有很多好的文章哦!