《Java1.8源碼分析》:HashMap

《Java1.8源碼分析》:HashMap

概要
HashMap 是一個關聯數組、哈希表,它是線程不安全的,允許key爲null,value爲null。遍歷時無序。
其底層數據結構是數組稱之爲哈希桶,每個桶裏面放的是鏈表,鏈表中的每個節點,就是哈希表中的每個元素。

在jdk8中,當鏈表長度爲8時,會轉爲紅黑樹,以提升它的查詢、插入效率。
因其底層哈希桶的數據結構是數組,所以也會涉及到擴容的問題。

當HashMap的容量達到threshold域值時,就會觸發擴容。擴容前後,哈希桶的長度一定會是2的次方
這樣在根據key的hash值尋找對應的哈希桶時,可以用位運算替代取餘操作,更加高效。

而key的hash值,並不僅僅只是key對象的hashCode()方法的返回值,還會經過擾動函數的擾動,以使hash值更加均衡

因爲hashCode()是int類型,取值範圍是40多億,只要哈希函數映射的比較均勻鬆散,碰撞機率是很小的。
但就算原本的hashCode()取得很好,每個key的hashCode()不同,但是由於HashMap的哈希桶的長度遠比hash取值範圍小,默認是16,所以當對hash值以桶的長度取餘,以找到存放該key的桶的下標時,由於取餘是通過與操作完成的,會忽略hash值的高位。因此只有hashCode()的低位參加運算,發生不同的hash值,但是得到的index相同的情況的機率會大大增加,這種情況稱之爲hash碰撞。 即,碰撞率會增大。

擾動函數就是爲了解決hash碰撞的。它會綜合hash值高位和低位的特徵,並存放在低位,因此在與運算時,相當於高低位一起參與了運算,以減少hash碰撞的概率。(在JDK8之前,擾動函數會擾動四次,JDK8簡化了這個操作)

擴容操作時,會new一個新的Node數組作爲哈希桶,然後將原哈希表中的所有數據(Node節點)移動到新的哈希桶中,相當於對原哈希表中所有的數據重新做了一個put操作。所以性能消耗很大,可想而知,在哈希表的容量越大時,性能消耗越明顯。

因爲擴容是容量翻倍,所以原鏈表上的每個節點,現在可能存放在原來的下標,即low位, 或者擴容後的下標,即high位。 high位= low位+原哈希桶容量
如果追加節點後,鏈表數量》=8,則轉化爲紅黑樹

這裏寫圖片描述

繼承結構

public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable {

HashMap繼承了AbstractMap及實現了Map、Cloneable和Serializable接口。
看過源碼的人可能都有這樣一個疑問:AbstractMap也實現了Map接口,爲什麼HashMap既繼承AbstractMap抽象類還需要實現Map接口嗎???

從功能上來說:HashMap實現Map是沒有任何作用的。

從結構上來說:由於我們一般是面對接口編程,爲了維護結構清晰和完整,是需要實現Map接口的。

而HashMap繼承AbstractMap的作用爲:AbstractMap 提供 Map 接口的骨幹實現,以最大限度地減少實現此接口所需的工作。

屬性

//HashMap初始的長度
1. static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
//最大長度2^31
2. static final int MAXIMUM_CAPACITY = 1 << 30;
//默認加載因子
3. static final float DEFAULT_LOAD_FACTOR = 0.75f;
//鏈表轉成紅黑樹的閾值
4. static final int TREEIFY_THRESHOLD = 8;
//紅黑樹轉爲鏈表的閾值
5. static final int UNTREEIFY_THRESHOLD = 6;
//存儲方式由鏈表轉成紅黑樹的容量的最小閾值
6. static final int MIN_TREEIFY_CAPACITY = 64;
//HashMap中存儲的鍵值對的數量
7. transient int size;
//擴容閾值,當size>=threshold時,就會擴容
8. int threshold;
//HashMap的加載因子
9. final float loadFactor;

這裏我們需要加載因子(load_factor),加載因子默認爲0.75,當HashMap中存儲的元素的數量大於(容量×加載因子),也就是默認大於16*0.75=12時,HashMap會進行擴容的操作。

構造函數
hashMap構造函數有4個

public HashMap() {
        //默認構造函數,賦值加載因子爲默認的0.75f
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }

public HashMap(int initialCapacity) {
    //指定初始化容量的構造函數
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

//同時指定初始化容量 以及 加載因子, 用的很少,一般不會修改loadFactor
public HashMap(int initialCapacity, float loadFactor) {
    //邊界處理
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " +
                                           initialCapacity);
    //初始容量最大不能超過2的30次方
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    //顯然加載因子不能爲負數
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " +
                                           loadFactor);
    this.loadFactor = loadFactor;
    //設置閾值爲  》=初始化容量的 2的n次方的值
    this.threshold = tableSizeFor(initialCapacity);
}

//新建一個哈希表,同時將另一個map m 裏的所有元素加入表中
public HashMap(Map<? extends K, ? extends V> m) {
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    putMapEntries(m, false);
}
 //根據期望容量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;
    }
//將另一個Map的所有元素加入表中,參數evict初始化時爲false,其他情況爲true
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
    //拿到m的元素數量
    int s = m.size();
    //如果數量大於0
    if (s > 0) {
        //如果當前表是空的
        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);
            //如果新的閾值大於當前閾值
            if (t > threshold)
                //返回一個 》=新的閾值的 滿足2的n次方的閾值
                threshold = tableSizeFor(t);
        }
        //如果當前元素表不是空的,但是 m的元素數量大於閾值,說明一定要擴容。
        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);
        }
    }
}

擴容函數

final Node<K,V>[] resize() {
        //oldTab 爲當前表的哈希桶
        Node<K,V>[] oldTab = table;
        //當前哈希桶的容量 length
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        //當前的閾值
        int oldThr = threshold;
        //初始化新的容量和閾值爲0
        int newCap, newThr = 0;
        //如果當前容量大於0
        if (oldCap > 0) {
            //如果當前容量已經到達上限
            if (oldCap >= MAXIMUM_CAPACITY) {
                //則設置閾值是2的31次方-1
                threshold = Integer.MAX_VALUE;
                //同時返回當前的哈希桶,不再擴容
                return oldTab;
            }//否則新的容量爲舊的容量的兩倍。 
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)//如果舊的容量大於等於默認初始容量16
                //那麼新的閾值也等於舊的閾值的兩倍
                newThr = oldThr << 1; // double threshold
        }//如果當前表是空的,但是有閾值。代表是初始化時指定了容量、閾值的情況
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;//那麼新表的容量就等於舊的閾值
        else {}//如果當前表是空的,而且也沒有閾值。代表是初始化時沒有任何容量/閾值參數的情況               // zero initial threshold signifies using defaults
            newCap = DEFAULT_INITIAL_CAPACITY;//此時新表的容量爲默認的容量 16
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);//新的閾值爲默認容量16 * 默認加載因子0.75f = 12
        }
        if (newThr == 0) {//如果新的閾值是0,對應的是  當前表是空的,但是有閾值的情況
            float ft = (float)newCap * loadFactor;//根據新表容量 和 加載因子 求出新的閾值
            //進行越界修復
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        //更新閾值 
        threshold = newThr;
        @SuppressWarnings({"rawtypes","unchecked"})
        //根據新的容量 構建新的哈希桶
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        //更新哈希桶引用
        table = newTab;
        //如果以前的哈希桶中有元素
        //下面開始將當前哈希桶中的所有節點轉移到新的哈希桶中
        if (oldTab != null) {
            //遍歷老的哈希桶
            for (int j = 0; j < oldCap; ++j) {
                //取出當前的節點 e
                Node<K,V> e;
                //如果當前桶中有元素,則將鏈表賦值給e
                if ((e = oldTab[j]) != null) {
                    //將原哈希桶置空以便GC
                    oldTab[j] = null;
                    //如果當前鏈表中就一個元素,(沒有發生哈希碰撞)
                    if (e.next == null)
                        //直接將這個元素放置在新的哈希桶裏。
                        //注意這裏取下標 是用 哈希值 與 桶的長度-1 。 由於桶的長度是2的n次方,這麼做其實是等於 一個模運算。但是效率更高
                        newTab[e.hash & (newCap - 1)] = e;
                        //如果發生過哈希碰撞 ,而且是節點數超過8個,轉化成了紅黑樹(暫且不談 避免過於複雜, 後續專門研究一下紅黑樹)
                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    //如果發生過哈希碰撞,節點數小於8個。則要根據鏈表上每個節點的哈希值,依次放入新哈希桶對應下標位置。
                    else { // preserve order
                        //因爲擴容是容量翻倍,所以原鏈表上的每個節點,現在可能存放在原來的下標,即low位, 或者擴容後的下標,即high位。 high位=  low位+原哈希桶容量
                        //低位鏈表的頭結點、尾節點
                        Node<K,V> loHead = null, loTail = null;
                        //高位鏈表的頭節點、尾節點
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;//臨時節點 存放e的下一個節點
                        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);
                        //將低位鏈表存放在原index處,
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        //將高位鏈表存放在新index處
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

再看一下 往哈希表裏插入一個節點的putVal函數,如果參數onlyIfAbsent是true,那麼不會覆蓋相同key的值value。如果evict是false。那麼表示是在初始化時調用的
小結:
* 運算儘量都用位運算代替,更高效。
* 對於擴容導致需要新建數組存放更多元素時,除了要將老數組中的元素遷移過來,也記得將老數組中的引用置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重寫使用。

put函數

public V put(K key, V value) {
        //先根據key,取得hash值。 再調用上一節的方法插入節點
        return putVal(hash(key), key, value, false, true);
    }

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

remove函數

public V remove(Object key) {
        Node<K,V> e;
        return (e = removeNode(hash(key), key, null, false, true)) == null ?
            null : e.value;
    }
final Node<K,V> removeNode(int hash, Object key, Object value,
                               boolean matchValue, boolean movable) {
        // p是待刪除節點的前置節點
        Node<K,V>[] tab; Node<K,V> p; int n, index;
        //如果哈希表不爲空,則根據hash值算出的index下 有節點的話。
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (p = tab[index = (n - 1) & hash]) != null) {
            //node是待刪除節點
            Node<K,V> node = null, e; K k; V v;
            //如果鏈表頭的就是需要刪除的節點
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                node = p;//將待刪除節點引用賦給node
            else if ((e = p.next) != null) {//否則循環遍歷 找到待刪除節點,賦值給node
                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 = e;
                    } while ((e = e.next) != null);
                }
            }
            //如果有待刪除節點node,  且 matchValue爲false,或者值也相等
            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)//如果node ==  p,說明是鏈表頭是待刪除節點
                    tab[index] = node.next;
                else//否則待刪除節點在表中間
                    p.next = node.next;
                ++modCount;//修改modCount
                --size;//修改size
                afterNodeRemoval(node);//LinkedHashMap回調函數
                return node;
            }
        }
        return null;
    }

get函數

public V get(Object key) {
        Node<K,V> e;
        //傳入擾動後的哈希值 和 key 找到目標節點Node
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }
//傳入擾動後的哈希值 和 key 找到目標節點Node
final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    //查找過程和刪除基本差不多, 找到返回節點,否則返回null
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != 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)
                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;
}

與HashTable的區別

  • 與之相比HashTable是線程安全的,且不允許key、value是null。 HashTable默認容量是11。
  • HashTable是直接使用key的hashCode(key.hashCode())作爲hash值,不像HashMap內部使用static
  • final int hash(Object key)擾動函數對key的hashCode進行擾動後作爲hash值。
  • HashTable取哈希桶下標是直接用模運算%.(因爲其默認容量也不是2的n次方。所以也無法用位運算替代模運算)
  • 擴容時,新容量是原來的2倍+1。int newCapacity = (oldCapacity << 1) + 1;
  • Hashtable是Dictionary的子類同時也實現了Map接口,HashMap是Map接口的一個實現類;

引用
https://blog.csdn.net/zxt0601/article/details/77413921
https://blog.csdn.net/u010412719/article/details/51980632

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