一文解讀JDK8中HashMap的源碼

前言

HashMap是平時開發中非常常用的一個集合框架類,瞭解其內部構造及運行原理可以說是每一個Java程序員必不可少的,接下來就從源碼一探究竟HashMap到底是什麼樣的類。

一、HashMap簡介

HashMap是java.util包中的一個集合框架類,它是java.util.Map的實現類,具有方便、高效的基於鍵值對存取的功能,其平均查詢時間複雜度爲O(1),非線性安全。

HashMap是一種用哈希表 + 鏈表 + 紅黑樹等數據結構實現的基於key-value存取的工具類,在JDK1.8之前沒有紅黑樹這一數據結構,在JDK1.8之後對其進行了優化:考慮到發生大量Hash碰撞時鏈表查詢效率低,所以加入了紅黑樹這一數據結構以提高此種情況的查詢效率,通過閾值控制,將鏈表和紅黑樹進行相互轉化同時JDK1.8還有一處優化,即hash擾動函數的優化,在JDK1.8之前hash()函數中對key的hash值擾動了四次,目的是降低hash碰撞的可能性,但是JDK1.8之後只進行了一次擾動,實現方式進行了簡化

在這裏插入圖片描述

話不多說,接下來就直接進入源碼解析部分吧

二、HashMap源碼解讀

溫馨提示:閱讀需耐心哦~~

1. 類定義

HashMap是Map類的實現類,同時繼承了AbstractMap類、Cloneable類、Serializable類,後面兩個標誌性的接口賦予了它可克隆、可序列化的能力。

//  HashMap類,繼承自AbstractMap,實現了Map接口
//  並且實現了兩個標誌性接口,賦予了它可克隆、可序列化的能力
public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable {
}

2. 常量定義

//  序列化ID,作爲唯一識別標誌,用於序列化和反序列化    
private static final long serialVersionUID = 362498820763181265L;

//  默認初始化容量大小,爲16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

//  最大容量:2的30次方
static final int MAXIMUM_CAPACITY = 1 << 30;

//  負載因子,在擴容時使用
static final float DEFAULT_LOAD_FACTOR = 0.75f;

//  一個桶的樹化閾值
//  當桶中元素個數超過這個值時,需要使用紅黑樹節點替換鏈表節點
static final int TREEIFY_THRESHOLD = 8;

//  一個樹的鏈表還原閾值
//  當擴容時,桶中元素個數小於這個值,就會把樹形的桶元素 還原(切分)爲鏈表結構
static final int UNTREEIFY_THRESHOLD = 6;

//  哈希表的最小樹形化容量
//  當哈希表中的容量大於這個值時,表中的桶才能進行樹形化
//  否則桶內元素太多時會擴容,而不是樹形化
//  爲了避免進行擴容、樹形化選擇的衝突,
//  這個值不能小於 4 * TREEIFY_THRESHOLD
static final int MIN_TREEIFY_CAPACITY = 64;

3. 內部類Node

HashMap中有很多內部類,比如Node、TreeNode、KeySet、Values、EntrySet、HashIterator等,因爲本文僅涉及增刪改查操作,Node類、TreeNode類是其中操作的主要類,但是由於紅黑樹較爲複雜,不是本文的重點,所以這裏暫只解讀Node類。

Node類是鏈表中存儲的節點類,用於存儲節點hash、key、value等信息,當然還有下一個節點的引用

//  實際的存儲節點內部類,可存在於紅黑樹也可存在於連表中
static class Node<K,V> implements Map.Entry<K,V> {
    final int hash; //  該節點的hash值
    final K key;    //  鍵
    V value;        //  值
    Node<K,V> next; //  下一個節點,樹節點或連表節點

    //  構造函數
    Node(int hash, K key, V value, Node<K,V> next) {
        this.hash = hash;
        this.key = key;
        this.value = value;
        this.next = next;
    }

    //  獲取鍵和值以及重寫的toString方法等
    public final K getKey()        { return key; }
    public final V getValue()      { return value; }
    public final String toString() { return key + "=" + value; }

    //  重寫hashCode方法
    public final int hashCode() {
        //  返回鍵的hash與值的hash求異或的結果,保證節點hash的唯一性
        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) {

        //  如果該類和本類的內存地址相同則直接返回true
        if (o == this)
            return true;

        //  先判斷是否是Map.Entry類型的類
        //  然後分別比較key和value是否相同
        //  如果都相同則返回true,否則返回false
        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;
    }
}

4. 靜態工具方法

HashMap中提供了四個公告的靜態工具方法,分別是hash、comparableClassFor、compareComparables、tableSizeFor。

  • hash:hash擾動函數,用於計算出key的hash值,其中進行了一次擾動,以減少hash碰撞的概率。
//  擾動函數,獲取key的hash值
//  該方法相比於JDK7的四次位移已經做出了優化
//  只需1次位移即可實現,原理相同
static final int hash(Object key) {
    int h;
    //  如果key不爲空
    //  那麼就對key的hash進行一次16位無符號右位移異或混合然後返回
    //  這樣擾動一次的目的是在於減少hash碰撞的概率
    //  具體優化講解請移步:https://www.zhihu.com/question/20733617/answer/111577937
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
  • comparableClassFor:用於檢查某個對象是否可比較,在HashMap中多用於key的檢查。其中對String進行了特判,String類實現了Comparable類,並且重寫了Object的hashCode()和equals()方法,所以平常都建議大家用String類作爲key。
//  用於檢查某個對象是否可比較,在HashMap中多用於key的檢查
static Class<?> comparableClassFor(Object x) {
    //  先判斷是否是Comparable類型的,如果不是則表明對象不可比較
    if (x instanceof Comparable) {
        Class<?> c; Type[] ts, as; Type t; ParameterizedType p;
        //  對String類型特判,如果是String類型直接返回該對象的Class類
        //  所以大多數人都建議在使用HashMap時key使用String類型
        if ((c = x.getClass()) == String.class) // bypass checks
            return c;
        //  獲取該類實現的接口集,包含泛型參數信息
        //  前提是保證它實現了某些接口才有可能實現Comparable
        if ((ts = c.getGenericInterfaces()) != null) {
            //  循環遍歷它實現的這些接口
            for (int i = 0; i < ts.length; ++i) {
                //  判斷其是否支持泛型
                if (((t = ts[i]) instanceof ParameterizedType) &&
                //  判斷承載該泛型信息的對象是否是Comparable類
                    ((p = (ParameterizedType)t).getRawType() ==
                        Comparable.class) &&
                //  獲取其實際泛型列表,並且有且只有一個泛型類,即c
                //  c是傳入對象x的類型
                    (as = p.getActualTypeArguments()) != null &&
                    as.length == 1 && as[0] == c) // type arg is c
                    return c;
            }
        }
    }
    return null;
}
  • compareComparables:比較k和x,如果x和k不是同一種類型就返回0,如果是同一類型那麼就返回其compareTo得到的值
//  比較k和x,如果x和k不是同一種類型就返回0
//  如果是同一類型那麼就返回其compareTo得到的值
@SuppressWarnings({"rawtypes","unchecked"}) // for cast to Comparable
static int compareComparables(Class<?> kc, Object k, Object x) {
    return (x == null || x.getClass() != kc ? 0 :
            ((Comparable)k).compareTo(x));
}
  • tableSizeFor:跟據期望容量cap,計算2的n次方形式的哈希桶的實際容量。
//  根據期望容量cap,返回2的n次方形式的哈希桶的實際容量 length。 
//  返回值一般會>=cap 
static final int tableSizeFor(int cap) {
    //  經過下面的 或 和位移 運算, n最終各位都是1。
    int n = cap - 1;
    n |= n >>> 1;
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
    //  判斷n是否越界,返回 2的n次方作爲 table(哈希桶)的閾值
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

5. 屬性變量

HashMap中定義了六個屬性變量,用於構建及管理hash表

//  Hash表,是一個Node類型的數組,每一個數組元就是一個桶
//  在第一次被使用時初始化,同時擴容時會對其進行數組遷移等操作
transient Node<K,V>[] table;

    // 緩存Node節點的集合,用於記錄被使用過的key和value的集合
transient Set<Map.Entry<K,V>> entrySet;

/**
    * The number of key-value mappings contained in this map.
    */
    // 已經存在的key-value對的數量
transient int size;

//  結構修改計數,rehash時會記錄
//  因爲hashmap也是線程不安全的,所以要保存modCount。用於fail-fast策略
transient int modCount;

//  調整容量的下一個大小值,其值等於 容量*負載因子
int threshold;

//  hash表的負載因子,用於計算哈希表元素數量的閾值。  
//  threshold = 哈希桶.length * loadFactor;
final float loadFactor;

6. 構造函數

//  傳入初始化容量和負載因子的構造函數
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;
    //  根據預期容量計算實際容量
    //  實際容量大小始終是大於等於預期容量的最小的2的次冪
    //  比如傳入初始化容量是7,計算得到實際的容量是8,8是2的3次方
    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
}

//  構造一個新的HashMap,同時將傳入的Map m的所有元素加入到表中
public HashMap(Map<? extends K, ? extends V> m) {
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    putMapEntries(m, false);
}

其中批量注入元素的方法putMapEntries如下:

//  將另一個Map的所有元素加入表中
//  參數evict初始化時爲false
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
    //  獲取m元素的數量
    int s = m.size();
    //  數量大於0才進行操作
    if (s > 0) {
        //  如果此時hash表未初始化
        if (table == null) { // pre-size
            //  根據m中大小計算和負載因子,計算出閾值
            float ft = ((float)s / loadFactor) + 1.0F;
            //  修正閾值的邊界 不能超過MAXIMUM_CAPACITY
            int t = ((ft < (float)MAXIMUM_CAPACITY) ?
                        (int)ft : MAXIMUM_CAPACITY);
            //  如果新的閾值大於當前閾值
            //  那麼返回一個大於等於新的閾值的滿足2的n次方的閾值
            if (t > threshold)
                threshold = tableSizeFor(t);
        }
        //  如果表格不爲空,並且m的元素數量大於了當前容量閾值
        //  則進行resize擴容
        else if (s > threshold)
            resize();
        //  遍歷 m 依次將元素加入當前表中
        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);
        }
    }
}

7. 幾個常規方法(不完整)

//  經常使用的,獲取hash表中已經存在的鍵值對數量
//  注意這裏的size並非是hash表的大小,而是實際存在的鍵值對數量
public int size() {
    return size;
}

//  是否爲空,即是否實際存在鍵值對(與table容量無關)
public boolean isEmpty() {
    return size == 0;
}
//  檢測是否存在key
//  邏輯和get類似,主要是調用getNode方法
public boolean containsKey(Object key) {
    return getNode(hash(key), key) != null;
}
//  檢測是否存在value
public boolean containsValue(Object value) {
    Node<K,V>[] tab; V v;
    //遍歷哈希桶上的每一個鏈表
    if ((tab = table) != null && size > 0) {
        for (int i = 0; i < tab.length; ++i) {
            for (Node<K,V> e = tab[i]; e != null; e = e.next) {
                //如果找到value一致的返回true
                if ((v = e.value) == value ||
                    (value != null && value.equals(v)))
                    return true;
            }
        }
    }
    return false;
}
//  批量存入一個Map,邏輯和構造函數中相同,主要調用putMapEntries
public void putAll(Map<? extends K, ? extends V> m) {
    putMapEntries(m, true);
}

8. 查詢方法 get()

根據key查詢,找到返回value,沒找到返回null

//  根據key獲取值
public V get(Object key) {
    Node<K,V> e;
    //  根據key的值和擾動後key的hash值先得到Node節點,然後獲取其中的值
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}

//  JDK8新增的方法,查詢到則返回其value,沒有則返回設定的缺省值
public V getOrDefault(Object key, V defaultValue) {
    Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? defaultValue : e.value;
}

//  根據擾動後的hash值和key的值獲取節點
final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    //  基本邏輯:先找到相應節點,然後返回,如果不存在返回null

    //  table不爲空並且其大小大於0才繼續
    if ((tab = table) != null && (n = tab.length) > 0 &&
        //  hash和n-1進行區域後定位到桶的位置,然後獲取其頭結點first
        (first = tab[(n - 1) & hash]) != null) {
        //  如果頭結點恰好是該節點則直接返回
        //  檢測內容:頭節點的hash是否相同,key是否相同(檢測內存地址或檢測值)
        if (first.hash == hash && // always check first node
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        //  頭結點不是要找的節點,接下來取得下一個節點進行尋找
        if ((e = first.next) != null) {
            //  如果桶內的數據結構是紅黑樹,那麼就調用getTreeNode方法去查找
            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;
}

9. 插入/更新方法 put()

在這裏插入圖片描述

向表中插入或更新一個值,其邏輯如下

  1. 檢查hash表是否初始化,如果沒有就進行resize擴容
  2. 根據key的擾動hash值定位到桶的位置,如果桶內爲空,直接創建新的Node放入桶中
  3. 如果桶不爲空,則發生了hash碰撞,則進行下一步操作:
    • 如果桶內數據結構是紅黑樹,則以紅黑樹的形式遍歷。如果key不存在則直接插入,如果key存在先獲取到該節點
    • 如果桶內數據結構是鏈表,則以鏈表的形式循環遍歷。如果遍歷到尾節點仍無相同key存在,則直接插入,並且檢測是否超過閾值,決定是否需要樹化;如果key已經存在,則先獲取該節點
  4. 如果允許覆蓋,則將之前找到的key對應的節點值進行覆蓋,否則什麼也不做
  5. 修改操作計數、鍵值對數量等信息,並檢測是否需要擴容,如果需要則進行resize
//  插入新的值,主要調用putVal方法,詳細邏輯見putVal()
public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

//  插入新值核心函數
//  如果參數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;
    //  首先檢查table是否是null並且容量是否大於0,即有沒有初始化table
    //  如果沒有初始化就進行resize擴容
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    //  先定位到桶的位置,p爲該桶的頭節點
    //  如果p爲null則說明該桶還沒有節點,直接將新鍵值對存入桶中
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    //  桶內頭節點p不爲空,即發生了hash碰撞,進一步處理
    else {
        Node<K,V> e; K k;
        //  比較頭節點的擾動hash值及key值
        //  如果相同則說明存入的節點key已存在,而且就是頭節點
        //  先獲取該節點,是否覆蓋其值進一步處理
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        //  頭節點的key和插入的key不相同
        //  先判斷桶內數據結構是否是紅黑樹,如果是則以紅黑樹的方式插入到樹中
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        //  桶內節點不是紅黑樹,即鏈表結構
        else {
            //  循環遍歷該鏈表
            //  直到找到與插入節點key相同的節點,沒找到就直接插入到尾結點
            for (int binCount = 0; ; ++binCount) {
                //  已經遍歷到了尾節點,說明插入的key不存在,直接插入到尾部
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    //  如果桶內節點數量達到了樹型化閾值,則進行樹型化
                    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;
            }
        }
        //  如果獲取到的節點不爲null則進行操作
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            //  方法傳入的onlyIfAbsent參數爲false,或者舊值爲null則直接替換掉舊值
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            //  這是一個空實現的函數,用作LinkedHashMap重寫使用
            afterNodeAccess(e);
            return oldValue;
        }
    }

    //  以上操作以及全部完成,並且已經成功插入或更改一個節點的值
    //  修改modCount的值,記錄修改次數
    ++modCount;
    //  更新size,並判斷如果超過了閾值則進行擴容
    if (++size > threshold)
        resize();
    //  這是一個空實現的函數,用作LinkedHashMap重寫使用
    afterNodeInsertion(evict);
    return null;
}

10. 桶的樹形化 treeifyBin()

如果一個桶中的元素個數超過 TREEIFY_THRESHOLD(默認是 8 ),就使用紅黑樹來替換鏈表,提高查詢效率

//將桶內所有的鏈表節點替換成紅黑樹節點
final void treeifyBin(Node[] tab, int hash) {
    int n, index; Node e;
    //如果當前哈希表爲空,或者哈希表中元素的個數小於進行樹形化的閾值(默認爲 64),就去新建/擴容
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        resize();
    else if ((e = tab[index = (n - 1) & hash]) != null) {
        //如果哈希表中的元素個數超過了樹形化閾值,進行樹形化
        // e 是哈希表中指定位置桶裏的鏈表節點,從第一個開始
        TreeNode hd = null, tl = null; //紅黑樹的頭、尾節點
        do {
            //新建一個樹形節點,內容和當前鏈表節點 e 一致
            TreeNode p = replacementTreeNode(e, null);
            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);
    }
}

11. 擴容 resize()

resize是非常重要的一個函數,它負責了HashMap中動態擴容的核心邏輯,其主要邏輯如下:

  1. 備份舊錶、舊錶容量、舊錶閾值,定義新表的容量、閾值
  2. 如果舊錶容量大於0
    • 如果舊錶容量已經達到上限,則設置閾值爲最大整數,不再進行擴容
    • 如果舊錶容量未達上限,設置新表容量爲舊錶容量的2倍,但前提是新表容量也得在上限範圍內
  3. 如果舊錶容量爲空,但是閾值大於0,說明初始化時指定了容量和閾值,舊錶的閾值則作爲新表的容量
  4. 如果舊錶容量爲空,並且閾值爲0,說明初始化時沒有指定容量和閾值,則將默認的初始容量和閾值作爲新表的容量和閾值
  5. 如果以上操作之後新表的閾值爲0,根據新表容量和負載因子求出新表的閾值
  6. 創建一個新的表,其數組長度爲新表容量
  7. 如果舊錶不爲空,就進行數據遷移,遷移時依次遍歷每個桶
    • 如果桶中只有一個節點,則直接放入新表對應位置的桶中
    • 如果桶中不止一個節點,並且結構是紅黑樹,則進行拆分紅黑樹然後遷移
    • 如果桶中不止一個節點,並且結構是鏈表,則分爲高位和低位分別遷移(高位= 低位 + 原哈希桶容量),低位放入新表對應舊錶桶索引中,高位放入新表對應新的桶索引中
  8. 返回新表
//  hash擴容核心函數
final Node<K,V>[] resize() {
    //  先存一箇舊table
    Node<K,V>[] oldTab = table;
    //  舊table的容量
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    //  舊table的閾值
    int oldThr = threshold;
    //  定義新table的容量和閾值
    int newCap, newThr = 0;
    //  如果舊table容量大於0
    if (oldCap > 0) {
        //  舊table容量已經達到上限,則設置閾值爲最大整數,不再進行擴容
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        } //  容量未達到上限,新table容量是舊table的2倍(前提是在上限範圍內)
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                    oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold
    }// 表示空的,但是閾值大於0,說明初始化時指定了容量、閾值
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;//  則直接把舊閾值作爲新table的容量
    else {  // 既沒有初始化容量又沒有初始化閾值,那麼就進行初始化
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;//根據新table容量和加載因子求出新的閾值
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                    (int)ft : Integer.MAX_VALUE);//   進行越界限定
    }
    //  更新閾值
    threshold = newThr;
    //  創建一個新的table
    @SuppressWarnings({"rawtypes","unchecked"})
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    //  把新的table直接賦值給table,原來存放值的table內存是被oldTab變量所指向
    table = newTab;
    //  如果舊table不爲空,那麼就進行節點遷移
    if (oldTab != null) {
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            //  依次獲取舊table中桶中的首節點
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;// 清理舊錶中該桶的內存空間,防止內存泄漏
                if (e.next == null)//   如果桶中只有一個節點,直接存入新table中
                    newTab[e.hash & (newCap - 1)] = e;
                else if (e instanceof TreeNode)//   桶中不止一個節點,並且結構是紅黑樹,則進行拆分
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                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;
                            // 利用哈希值和舊的容量取與,可以得到哈希值去模後,是大於等於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) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    //  高位鏈表存放在新的桶索引中
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

12. 刪除節點 remove()

刪除操作是根據key先找到對應的Node節點,然後再刪除,如果沒找到直接返回null,其操作和get()非常相似

//  根據key刪除一個節點,其主要是調用removeNode方法
public V remove(Object key) {
    Node<K,V> e;
    return (e = removeNode(hash(key), key, null, false, true)) == null ?
        null : e.value;
}

//  刪除節點的核心方法
//  如果參數matchValue是true,則必須key、value都相等才刪除。 
//  如果movable參數是false,在刪除節點時,不移動其他節點
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;
    //  在刪除之前先確認表是否爲空,並且其容量大於0
    //  同時根據key定位到桶位置中桶不爲空
    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;
        //  如果頭結點就是要刪除的節點,則直接賦值給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 {
                //  如果是鏈表,則循環遍歷查找
                //  注意此時p是刪除節點的前驅節點,node是被刪除的節點
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key ||
                            (key != null && key.equals(k)))) {
                        node = e;
                        break;
                    }
                    p = e;
                } while ((e = e.next) != null);
            }
        }
        //  如果要刪除的節點找到了,就進行刪除操作,否則返回null
        //  matchValue是true則要求key和value都必須相等
        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;//  鍵值對數量-1
            afterNodeRemoval(node);//  這是一個空實現的函數,LinkedHashMap回調函數
            return node;
        }
    }
    return null;
}

總結

HashMap的設計真的非常優秀,包括數組+鏈表+紅黑樹的組合、大量使用位運算提高效率、hash擾動減少碰撞等。不過HashMap最大的弊端就是不自持多線程,它的非線程安全性導致它無法在併發環境下有很好的表現,甚至會有出現致命的情況(比如resize造成死鎖)。HashTable雖然是線程安全,但是它的併發性能卻不見得很好,而ConcurrentHashMap則很好地彌補了這一短板,它在併發環境下有非常優秀的表現,後續小編也會出一篇JDK8的ConcurrentHashMap源碼解讀文章,和大家一起學習一下它在HashMap的基礎上又進行了哪些優化。

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