最新JDK8HashMap實現過程源碼分析

版權聲明:本文爲博主原創文章,未經博主允許不得轉載。 https://blog.csdn.net/youngogo/article/details/81267773

小編道行也沒那麼深,就用最通俗易懂的方式,來解釋hashmap實現原理。本文基於JDK8分析HashMap(),我們從源碼出發將主要分析討論如下的幾個知識點:

  1. HashMap的特點是什麼?以及它的使用場景
  2. HashMap的數據結構?
  3. HashMap的工作原理是什麼?
  4. equals和hashCode都有什麼作用?
  5. 重寫equals()爲什麼一定要重寫hashCode()?
  6. HashMap裏面的table數組爲什麼是2的N次方?

1、感知HashMap

 我們首先進行如下操作:

HashMap<String, Integer> map = new HashMap<String, Integer>();
map.put("語文", 1);
map.put("數學", 2);
map.put("英語", 3);
map.put("歷史", 4);
map.put("政治", 5);
map.put("地理", 6);
map.put("生物", 7);
map.put("化學", 8);
for(Entry<String, Integer> entry : map.entrySet()) {
    System.out.println(entry.getKey() + ": " + entry.getValue());
}

接着利用debug模式,從數據結構上認知HashMap,循序漸進

以下是JDK8中HashMap的數據結構源碼:

/**
     * The table, initialized on first use, and resized as
     * necessary. When allocated, length is always a power of two.
     * (We also tolerate length zero in some operations to allow
     * bootstrapping mechanics that are currently not needed.)
    表,在首次使用時初始化,並根據需要調整大小。當分配時,長度總是2的冪。
     */
    transient Node<K,V>[] table;

/**
     * Basic hash bin node, used for most entries.  (See below for
     * TreeNode subclass, and in LinkedHashMap for its Entry subclass.)
        基本的哈希bin節點,用於大多數條目(內部類)
     */
    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;
        }
    }

2、HashMap的兩個重要參數

 /**
     * The default initial capacity - MUST be a power of two.

        table的默認初始容量
     */
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

  /**
     * The load factor used when none specified in constructor.(負載因子)
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

capacity就是初始化table時的數組容量,load factor指table的填充比例;當我們對迭代性能要求比較高時,我們首先不能把capacity設置的太大;同時load factor不要超過0.75,否則會明顯增加衝突機率,降低HashMap性能

負載因子 * 容量 > 元素數量(put進去的元素個數)時,就需要調整容量(table的長度)爲原來的2倍

3、HashMap的put(Key k,Value v)的原理

在分析源碼之前,我們先看看大體思路:

 1)當在第一次put時,先對table初始化,通過hash計算得到存放位置table[i],存放。

 2)當再次put時,同樣經過hash計算得到位置,則採用鏈表法解決衝突,存放在相同位置的next區域

3)在JDK8中設置了鏈表的默認閾值爲8,如果超過這個值,則進行樹化

4)如果節點已經存在就替換old value(保證key的唯一性)

5)如果bucket滿了(超過load factor*current capacity),就要resize,變爲原來2倍

面試題:解釋HashMap的原理,數據量增大時,
     在數據量小的時候,HashMap是按照鏈表的模式存儲的。當數據量變大之後,爲了進行快速的查找,會將這個鏈表變成紅黑樹(均衡二叉樹),用hash碼作爲數據的定位來進行保存。

具體實現代碼如下:

public V put(K key, V value) {
    // 對key的hashCode()做hash
    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;
    // tab爲空則創建
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    // 計算index得出存放的位置,並對null做處理
    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;
    // 超過load factor*current capacity,resize()
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

4、get函數的實現

大致思路如下:

  1. bucket裏的第一個節點,直接命中;
  2. 如果有衝突,則通過key.equals(k)去查找對應的entry
    若爲樹,則在樹中通過key.equals(k)查找,O(logn);
    若爲鏈表,則在鏈表中通過key.equals(k)查找,O(n)。
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) {
            // 在樹中get
            if (first instanceof TreeNode)
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            // 在鏈表中get
            do {
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null;
}

爲了避免篇幅過長,關於table的長度爲什麼必須是2的N次方,hash函數的實現以及對開始問題的回答,在下篇博客進行分析,請轉最新JDK8HashMap實現原理(二)

歡迎大家留言交流,小編q:1298364867

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