HashMap的put和get數據流程揭祕

本文是針對JDK1.8中的HashMap,之前以爲已經懂的不錯了,結果發現很多關鍵點沒明白

1. 先說HashMap的數據結構

核心數據結構就三個:數組,鏈表,Tree

  • 數組Node<K,V> table
    數據就是個簡單的Node數組,存放的是當前索引應第一個Node<K,V>對節點,或者是空(說明沒有存放數據)
  • 鏈表
    如果掛在同一個索引下的數據Node個數小於變Tree閾值(默認是8個),那麼將以數組中的元素爲頭節點,依次掛成一個鏈表
  • Tree
    如果這個個數超過了變Tree閾值,將把key的hash()結果變成value,建立紅黑樹

2. 關鍵點

  • K V對進來時怎麼找到放到table中的位置?
    K V對進來時,放到數組中的位置是根據K的hashcode和當前數組的容量計算出來的一個索引值,這種計算方式是HashMap放置數據的核心。

計算在數組中的位置主要方式是:
首先,計算出K的hashcode,如果是String的話,那就是直接拿的Object的Native方法;
然後,取這個hashcode的高16位和低16位做作疑惑,得到一個hash值,爲啥要這麼做呢?源碼中解釋是有助於減少衝突;

	就是這樣計算的hash值:
	static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

最後,把hash與容量建議相與,即 index = (n - 1) & hash

這個index 就是新進來的K V對應該放的位置。

插播:
對象的hash算法和equal是存在一定關係的,不是隨便寫個hash算法和equal方法就可以了
對於同一個對象:
1. 相同對象,多次hashcode得到的int型數字應該是一樣的;
2. 如果兩個對象equal,那這兩個對象的hashcode必須一樣;
3. 兩個對象不equal,不一定hashcode不一樣

所以HashMap中如果Key是自定義對象的話,一定要重新hashcode方法和equal方法,不然本來相同的會拿到不同的hashcode,用equal時也會判斷不相等。

3. get取數據

核心的核心是根據Key值找到數組中對應的index,當時找index的方法就是2中所說的

瞭解到數據結構和運行原理後,相信基本能猜到HashMap裏面是怎麼個邏輯了

  1. 找到數組中的index,其實就是如果這個key有數據,那它會在數組中哪個開頭的鏈表或者樹上。
  2. 如果這個index上的頭爲空,那就是沒有;如果只有一個頭,但和它和key不相等,那也沒有;如果不止一個頭,說明是鏈表或者樹,根據first instanceof TreeNode來判斷是不是樹;
  3. 剩下的就是如果是鏈表就鏈表找,如果是樹就樹找,判斷找到的條件是,地址一樣或者equal

源碼中關鍵方法是:

get(Object key);
get(int hash, Object key);

    // 這個方法就是個入口,頂多是計算了從哪找
    public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }
// 這個方法纔是真正的找的方法
final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        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;
    }

4. put數據

put數據就比get要複雜些,但也沒什麼太神奇的,相信如果自己去實現,邏輯也是基本類似的。

首先,肯定是找Key放的位置,就是計算index,及數組中第一個元素;
然後,如果這個位置爲空,那new 一個就可以了;如果這個位置的頭結點是TreeNode呢,那它肯定已經變樹了,那按樹的方式放數據;如果不是呢,那就按鏈表的方式,放到鏈表最後面,爲啥不放到頭節點後面呢?因爲放完之後還要看要不要把鏈表變樹,肯定要遍歷一遍,所以應該這樣就直接放到尾節點了。如果達到了變樹的閾值,肯定就要變樹了。
最後,上面其實做的是放位置,放完位置還有檢查下是不是該擴容了,如果要擴容就麻煩點了,需要resize。

擴容的幾個關鍵點在於:

  1. 數組table擴成兩倍大小;
  2. 擴容之後node的位置,要麼是在原有的index上,要麼在原有數組大小+index的位置,就這兩種可能。
  3. 擴容的過程中首先造一個兩倍的數組,然後遍歷老的數組,對於每一個頭節點的要麼是單個數據,要是是鏈表,要麼是樹。單個數據的簡單,鏈表和樹的就會把原有的數據拆分兩部分,一部分放到index位置,一部分放到數組大小+index位置
     // 這個是常看到的,對我的,在這裏計算了hash
     public V put(K key, V value) {
            return putVal(hash(key), key, value, 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;
            if ((tab = table) == null || (n = tab.length) == 0)  // 這是table未初始化
                n = (tab = resize()).length;
            if ((p = tab[i = (n - 1) & hash]) == null)      // index上啥也沒有,直接加
                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;
        }

resize自己看羅

關鍵還要看源碼,要思考這玩意就應該我自己來寫,怎麼實現

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