HashMap源碼分析(基於JDK1.8)

看到網上對HashMap源碼分析的文章很多,大部分概念都是對的,但是沒有讓人理解哈希表的本質,今天畫了一些時間認真的看了一遍HashMap的源碼,所以想寫下這篇文章總結一下。

先來一張HashMap的底層數據結構圖:

HashMap

這張圖大家是很熟悉的,HashMap底層就是一個Node<K, V> [] table,源碼如下:

//用來存key-value對象
transient Node<K,V>[] table;

//其中Node<key, value>是HashMap的一個靜態內部類
static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;
        ...
}

HashMap中有幾個比較關鍵的常量需要我們瞭解一下:

//默認的初始化大小,也就是Node<K,V>[]的默認長度
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

//集合允許存放的元素最大個數
static final int MAXIMUM_CAPACITY = 1 << 30;

//默認裝載因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;

//當單條鏈表的長度大於等8並且容量大於64時,就會將鏈表轉換成紅黑樹
static final int TREEIFY_THRESHOLD = 8;

//當單條鏈表的長度小於等於6時,就會將紅黑樹轉換成鏈表
static final int UNTREEIFY_THRESHOLD = 6;

//當單條鏈表的長度大於等8並且容量大於64時,就會將鏈表轉換成紅黑樹
static final int MIN_TREEIFY_CAPACITY = 64;

//hash表的元素個數
transient int size;

//當size大於等於這個數時會進行rehash
int threshold;

//負載因子,如果沒有傳入則使用默認值 0.75f
final float loadFactor;

//記錄hash表的修改次數
transient int modCount;

一、HashMap的構造函數

HashMap提供了無參構造函數和幾個重載的有參構造函數,裏面做的事情都沒啥區別,就是給loadFactor和threshold賦初始值

源碼:

    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;
        /**
        * tableSizeFor方法用來將輸入的值轉換成2的整數倍,假如你輸入的初始大小爲7,則會
        * 幫你自動轉換成8,因爲HashMap中table的長度永遠是2的整數倍
        */
        
        this.threshold = tableSizeFor(initialCapacity);
    }

這裏的threshold 並不是最終的用來判斷是否需要resize的值,而是table的長度,此時的table也是null,在向HashMap中放入第一個key-value時,會初始化table,並重新計算threshold。

二:HashMap的put過程

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)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            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) {
                        p.next = newNode(hash, key, value, null);
                        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))))
                        break;
                    p = e;
                }
            }
            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);
        return null;
    }

總結起來就是以下幾步:

  1. 判斷table是否爲空,如果爲空則進行resize,rezise時會重新設置threshold的值;
  2. 判斷hash值對應的位置是否有元素,如果沒有則直接放在對應的位置;
  3. 如果已經有元素了則進行判斷:
    1. 如果第一個元素的key等於要加入的key,則直接e標記爲第一個元素
    2. 如果第一個元素是一個紅黑樹,則調用紅黑樹的put方法
    3. 對鏈表進行遍歷,如果key匹配,則將e標記成這個元素,否則將新插入的元素放在鏈表的末尾,注意不是表頭:
    4. 判斷鏈表的長度是否大於等於TREEIFY_THRESHOLD,如果是,則將鏈表轉換成紅黑樹
  4. 如果e不等於空,則將e對應的value更新,並將oldvalue返回
  5. 記錄hashMap的操作次數,判斷size如果大於負載因子,則進行resize。

基本上看懂了put的過程,get的過程就很簡單了,自己去看一下原碼就明白了。

再額外說一下爲什麼HashMap中table的長度要設置成2的整數倍,因爲我們是通過key的hash值來確定key對應的數組位置的,那麼如果對應了,我們肯定想到了取模,例如:table的長度是16,則對16取模就可以了,也就是hashcode % 16,但是取模效率是很低的,其實對於2的整數倍對任何數取模可以直接用&操作,上面的例子就可以改爲 hashcode & (16 - 1)。總結成公式就是hashcode % length = hashcode & (length - 1),這裏的&操作效率可比%高得多。

PS:負載因子默認是0.75,所以Map中的元素個數不會達到初始化的容量就會進行resize,我們在初始化HashMap,給定容量大小時一定要考慮這一點。

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