java8 HashMap源碼 詳細研讀

HashMap原理

目的:

單純分析和學習hashmap的實現,很少說與Hashtable、ConcurrentHashMap等的區別。java

 

基於 jdk1.8node

 

在面試中有些水平的公司比較喜歡問HashMap原理,其中涉及的點比較多,並且大多能造成連環炮形式的問題。程序員

       通常連環炮,一環不知道後面試官也就不問了,可是低層連環沒連上,恭喜扣分是大大的,連到比較深的時候,說不知道還好點,好比:面試

  1. 關於集合的

1.1Hashmap是否是有序的?   不是繼續算法

1.2有沒有有順序的Map?     TreeMap  LinkedHashMap數組

1.3它們是怎麼來保證順序的?   通常都要說到其源碼,要不說不清爲麼有序安全

1.4答兩個有序或以上的 繼續  你以爲它們有序的區別,那個比較好,在什麼場景用哪一個好?數據結構

1.4答一個也能夠問上面的場景  繼續併發

1.5你以爲有沒有更好或者更高效的實現方式?有app

1.6 答有  這個時候提及來可能就要跑到底層數據結構上去了

數據結構繼續衍生 到 算法等等。。。

就這一個遇到大佬問你,能把不少人連到懷疑人生

2.關於hash的

1.1  hashmap基本的節點結構?  Node  鍵值對

1.2  鍵是什麼樣的,我用字符串a那鍵就是a嘛?   不是會進行hash

1.3  如何hash的  這樣hash有什麼好處?   源碼hashmap的hash算法

1.4  Hash在java中主要做用是什麼?

1.5  Hashcode  equal相關   須要同時重寫?緣由?

1.6  equal引出的對象地址、string帶有字符串緩衝區、字符串常量池

等等。。。

3.關於線程安全問題、到concurrent包等

 

 

 

前面說這些就是想說,hashmap中用到的東西不少,深刻學習和理解對每一個想晉升的程序員來講基本是必須,同時由它引出的對比,也是無限多,有很大的必要學習

 

一.HashMap類加載

1.只有一些靜態屬性會進行賦值,具體每一個值什麼用,暫時無論

  

  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;

    static final int TREEIFY_THRESHOLD = 8;

    static final int UNTREEIFY_THRESHOLD = 6;

    static final int MIN_TREEIFY_CAPACITY = 64;

2.沒有靜態的代碼塊,不會直接運行

 

.開始使用,第一步咱們確定是初始化方法,先從默認的構造方法開始學習

 public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }
1.AbstractMap父類,構造方法也沒幹事不談
2.只是賦值loadFactor 0.75f 沒幹別的事
3.static final float DEFAULT_LOAD_FACTOR = 0.75f;
4.loadFactor屬性 做用先放着後面用到再看
5.沒幹別的事了

 

三.通常咱們的使用第二步就是put了

先看經常使用的put鍵值對,這個學完了,那麼其餘的put方法就沒什麼問題了,好比putAll、putIfAbsent、putMapEntries

同時put弄明白了 取值就是一個反向就簡單了

 public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

1.先對key進行hash計算,學一下

static final int hash(Object key) {
                int h;
                return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
            }
            1.1 看出key是能夠空的  hash爲0
            1.2 (h = key.hashCode()) ^ (h >>> 16)   第一步取key的hashcode值  關於更底層的hashcode是什麼 有興趣再看
              h ^ (h >>> 16)  第二步 高位參與運算
            這個hash值的重要性就不說了,這裏這麼幹是出於性能考慮,底層的移位和異或運算確定比加減乘除取模等效率好 
            hashcode是32位的,無符號右移16位,那生成的就是16位0加原高位的16位值, 就是對半了,異或計算也就變成了高16位和低16位進行異或,原高16位不變。這麼幹主要用於當hashmap 數組比較小的時候全部bit都參與運算了,防止hash衝突太大,
            所謂hash衝突是指不一樣的key計算出的hash是同樣的,好比a和97,這個確定是存在的沒毛病

2.putVal 

/**
     * Implements Map.put and related methods
     *
     * @param hash hash for key
     * @param key the key
     * @param value the value to put
     * @param onlyIfAbsent if true, don't change existing value   相同key是否是覆蓋值
     * @param evict if false, the table is in creation mode.    在hashmap中沒用
     * @return previous value, or null if none
     */
    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;
    }
    2.1 執行順序
        第一句  Node<K,V>[] tab; Node<K,V> p; int n, i; 申明變量
            Node是啥,學習一下:
                static class Node<K,V> implements Map.Entry<K,V> {
                    final int 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;
                    }

                    public final K getKey()        { return key; }
                    public final V getValue()      { return value; }
                    public final String toString() { return key + "=" + value; }

                    public final int hashCode() {
                        return Objects.hashCode(key) ^ Objects.hashCode(value);
                    }

                    public final V setValue(V newValue) {
                        V oldValue = value;
                        value = newValue;
                        return oldValue;
                    }

                    public final boolean equals(Object o) {
                        if (o == this)
                            return true;
                        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;
                    }
                }
            是內部的一個靜態類,看看就明白了,明顯是一個帶有3個值,hash、key、value和另外一個Node對象引用的HashMap子元素結構,即咱們裝的每一個鍵值對就用一個Node對象存放
            
        第二句 if ((tab = table) == null || (n = tab.length) == 0) 這句
            tab = table賦值,table如今是null的,so n = tab.length不運行了 運行這個if的代碼塊
        第三句 n = (tab = resize()).length;   從下面的執行知道 n=16
            調用resize(),返回Node數組,這個resize是一個很是重要的方法,咱們就依如今的對象狀態去看這個方法,不帶入其餘狀態,認真研究學習下
                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) {
                        if (oldCap >= MAXIMUM_CAPACITY) {
                            threshold = Integer.MAX_VALUE;
                            return oldTab;
                        }
                        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                                 oldCap >= DEFAULT_INITIAL_CAPACITY)
                            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;
                        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;
                    @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) {
                            Node<K,V> e;
                            if ((e = oldTab[j]) != null) {
                                oldTab[j] = null;
                                if (e.next == null)
                                    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;
                                        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;
                }
            resize  1.Node<K,V>[] oldTab = table; 在上面知道table是null的,so  oldTab也是null
                    2.int oldCap = (oldTab == null) ? 0 : oldTab.length;   oldCap=0
                    3.int oldThr = threshold;   threshold咱們沒賦值過,int初始0 , oldThr=threshold=0
                    4.int newCap, newThr = 0;  不談
                    5.if (oldCap > 0) {    oldCap=0  if不運行
                    6.else if (oldThr > 0)  oldThr=0  if也不運行
                    7.else {
                            newCap = DEFAULT_INITIAL_CAPACITY;       DEFAULT_INITIAL_CAPACITY靜態成員變量,初始 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4     so newCap=16
                            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);   static final float DEFAULT_LOAD_FACTOR = 0.75f;   0.75*16=12   newThr=12
                        }
                    8.  if (newThr == 0) {     newThr=12 if不運行
                    9.  threshold = newThr;    threshold = newThr=12
                    10. Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]   申明一個16個大小的Node數組
                    11. table = newTab;   看出來了吧,table是成員變量,也就代表,HashMap初始數據結構是一個16的Node數組
                    12.  if (oldTab != null) {   oldTab是1中賦值的null,if不運行
                    13.  return newTab;  返回16大小的node數組
                總結,這一波調用是初次調用其實沒幹別的事,就是定義了基本的數據結構是16個Node數組,可是這個方法不簡單,由於一些if沒走
                
        第四句     if ((p = tab[i = (n - 1) & hash]) == null)
            n=16   15&hash 結果確定是0-15,這裏就看出,這是在計算一個key應該在整個數據結構16的數組中的索引了,並賦值給i變量,後面無論總體結構n變多大,這種計算key所在的索引是很是棒的設計。
            如今的狀態是初始的 確定是null的吧  if運行
            
        第五句 tab[i] = newNode(hash, key, value, null); new一個節點Node,放在數組裏,i是第四句計算的索引
        第六句 else {  不運行
        第七句  ++modCount;   transient int modCount; 根據註釋能夠看出,這個是記錄數據結構變更次數的,put值確定是變了的
        第八句  if (++size > threshold)  size=1  threshold在調用resize時賦值12   if不運行
        第九句 afterNodeInsertion(evict);  沒幹事
        第十句   return null; 不談

 

 

3.putVal 再回頭詳走,第一遍幹了不少初始化的事有些東西還沒研究到

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;
    }
    
        第一句  Node<K,V>[] tab; Node<K,V> p; int n, i; 申明變量不談
        第二句 if ((tab = table) == null || (n = tab.length) == 0) 這句
            tab = table賦值,table如今是16數組 n=16  if不運行
        第三句 if ((p = tab[i = (n - 1) & hash]) == null) 
            再看就知道了判斷當前存的key計算出的索引位置是否是已經存過值了
            沒存過就新Node存  和上面一遍同樣   咱們當已經有值了
            有值其實就意味着發生hash衝突了  好比key分別是a和97 hashCode都是97 衝突
            所以此次咱們主要看下一個else裏面HashMap是怎麼處理衝突的
        第四句     else中內容  即衝突處理
            p是衝突時數組該索引位置的元素
                1. p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k)))
                    判斷新元素hash和key是否是都和p相同,相同表示存了同樣的key
                    直接賦值給e
                2. p instanceof TreeNode(紅黑樹,具體的紅黑樹算法這裏就不詳細寫了,有興趣能夠去學習)
                    怎麼猛然來個紅黑樹,再3裏說
                    判斷原來元素是否是 TreeNode 類型
                    TreeNode同樣是靜態內部類,再看看就是紅黑樹的節點,所以這個地方用到了紅黑樹
                    putTreeVal 向紅黑樹中添加元素
                    內部實現,存在相同key就返回賦值給e  不存在就添加並返回null 源碼就是紅黑樹算法
                3.key不一樣也不是紅黑樹
                     if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                    }
                    先不看再裏面的那個if,這個一看就知道了吧,明顯的鏈表啊,並且數據裏的這個元素是鏈表頭
                    整個循環,明顯是在從頭開始遍歷鏈表,找到相同key或鏈表找完了新元素掛鏈表最後
                    
                    但在其中還有這麼個if
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                        這是在鏈表找完了,且新元素已經掛在鏈表最後了有的一個判斷
                        判斷循環次數,其實就是鏈表長度,長度超過TREEIFY_THRESHOLD 默認8則運行treeifyBin(tab, hash);
                    就是這個方法把鏈表變成紅黑樹了,具體方法源碼不談了,學紅黑樹就能夠了
            
            最後判斷e是否是空,上面的衝突方案看出e不是空就是表示有相同的key進行value覆蓋就能夠,e空就是無相同key且完成了數據掛載
            
        
        總結此次再走一遍putVal就是爲了學習HashMap的衝突處理方案,也看出內存結構是數組、鏈表、紅黑樹組成的,紅黑樹是java8新引進,是基於性能的考慮,在衝突大時,紅黑樹算法會比鏈表綜合表現更好

 

 

4.resize 再詳走 putVal最後一段size>threshold  threshold初始12 ++size元素數量確定會有超12個的時候,這裏也就看出了threshold表明HashMap的容量,到上限就要擴容了,默認如今16數組,12元素上限

1.Node<K,V>[] oldTab = table;  16大小
        2.int oldCap = (oldTab == null) ? 0 : oldTab.length;   oldCap=16
        3.int oldThr = threshold;     12
        4.int newCap, newThr = 0;  不談
        5.if (oldCap > 0) {         oldCap=16運行  oldCap是總體結構數組大小
                if (oldCap >= MAXIMUM_CAPACITY) {    判斷數組大小是否是已經到上限1<<30
                    threshold = Integer.MAX_VALUE;  到達上線 threshold 賦值最大值 而後返回 表示以後就再也不幹別的事了,隨便存,隨便hash衝突去,就這麼大,無限增長紅黑樹節點了
                    return oldTab;
                }
                else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                         oldCap >= DEFAULT_INITIAL_CAPACITY)   賦值newCap爲2倍數組大小,判斷若是擴充2倍有沒到上限,且不擴充時容量是否大於默認的16
                    newThr = oldThr << 1; // double threshold   知足則賦值  容量改成24
            }
            這段看出到threshold容量了就進行2倍擴容
        6.if (newThr == 0) {    若是運行該if 0 表示5步中擴容2倍到上限或原數組大小小於16
            float ft = (float)newCap * loadFactor;      newCap如今是2倍原大小的*0.75   2倍數組大小時的容量
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);     判斷2倍數組大小和2倍後的容量是否是都小於最高值,是則賦值新容量,不是就用整形最大值
            }
        7.  threshold = newThr;  把5 6兩步算出的新容量賦值給HashMap  也說明要擴容了
        8.   Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
            和後面的循環主要就是把原數組中的元素,一個一個添加到新數組中,轉移的一個過程
        
    總結,這一波調用是瞭解HashMap的擴容方式,看下來就是2倍擴容直到上限

 

5.總結,到這put就比較詳細了,也知道了基本結構是數組、鏈表、紅黑樹,鏈表到8個時轉換成紅黑樹
同時每次進行2倍擴容和數據轉移,擴容是用新結構的那顯然減小擴容次數會有更好的性能
那就要求每次聲明HashMap時最好是指定大小的

 

3、一些其餘咱們須要知道的


1.指定大小的初始化

   

public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
    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;
        this.threshold = tableSizeFor(initialCapacity);
    }
    第一個經常使用,第二個建議是不用,不去動0.75的這個容量比例,固然不絕對
    這裏tableSizeFor是一個很神奇的算法,我很是佩服的一個算法
        static final int tableSizeFor(int cap) {
            int n = cap - 1;
            n |= n >>> 1;
            n |= n >>> 2;
            n |= n >>> 4;
            n |= n >>> 8;
            n |= n >>> 16;
            return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
        }
        這個方法是在找大於等於cap且最小2的冪
        好比cap=1   結果 2 0次方 1
        cap=2  2
        cap=3 4
        cap=9  16
        分析下等於9
        cap - 1  第一步結果8
        00000000000000000000000000001000    8
        00000000000000000000000000000100    右移1位 
        
        00000000000000000000000000001100    或運算 結果
        00000000000000000000000000000011    右移2位
        00000000000000000000000000001111    或運算 結果
                                            
        00000000000000000000000000001111    右移 4 8 16沒用全是0結果仍是這個15
        最終 +1   16
        
        分析下等於大點 12345678
        00000000101111000110000101001110  12345678
        00000000101111000110000101001101  -1結果   12345677
        00000000010111100011000010100110  右移1位 
        
        00000000111111100111000111101111  或運算 結果
        00000000001111111001110001111011  右移2位
        
        00000000111111111111110111111111  差很少了在移0就沒了都是1了,+1不是確定是2的倍數了
        
        再說開始-1緣由這是爲了防止,cap已是2的冪。
        若是cap已是2的冪, 又沒有執行這個減1操做,則執行完後面的幾條無符號右移操做以後,返回的capacity將是這個cap的2倍。若是不懂,要看完後面的幾個無符號右移以後再回來看看

 

 

 

2.HashMap數組結構爲何用2的倍數 高速的索引計算,使用HashMap確定是衝突越少越好,就要求分部均勻,最好的用取模 h % length,可是近一步若是用2的冪h & (length - 1) == h % length 是等價的,效率缺差卻別很是大 綜合衡量用空間換了時間,且是值得的 3.線程安全問題 線程不安全,就put來看全程沒考慮線程問題,確定不安全,如今隨便併發一下resize會混亂吧,put鏈表,紅黑樹掛載基本都會出問題

相關文章
相關標籤/搜索