Java HashMap底層實現原理源碼分析Jdk8

在JDK1.6,JDK1.7中,HashMap採用位桶+鏈表實現,即便用鏈表處理衝突,同一hash值的鏈表都存儲在一個鏈表裏。可是當位於一個桶中的元素較多,即hash值相等的元素較多時,經過key值依次查找的效率較低。而JDK1.8中,HashMap採用位桶+鏈表+紅黑樹實現,當鏈表長度超過閾值(8)時,可能會將鏈表轉換爲紅黑樹,這樣大大減小了查找時間。html

簡單說下HashMap的實現原理:

首先存在一個table數組,裏面每一個元素都是一個node鏈表,當添加一個元素(key-value)時,就首先計算元素key的hash值,經過table的長度和key的hash值進行與運算獲得一個index,以此肯定插入數組中的位置,可是可能存在同一hash值的元素已經被放在數組同一位置了,這時就把這個元素添加到同一hash值的node鏈表的鏈尾,他們在數組的同一位置,可是造成了鏈表,同一各鏈表上的Hash值是相同的,因此說數組存放的是鏈表。而當鏈表長度大於等於8時,鏈表就可能轉換爲紅黑樹,這樣大大提升了查找的效率。java

存儲結構

存儲結構

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;
        }
        *
        *
        *
    }
transient Node<K,V>[] table;
  • HashMap內部包含一個Node類型的數組table,Node由Map.Entry繼承而來。
  • Node存儲着鍵值對。它包含四個字段,從next字段咱們能夠看出node是一個鏈表。
  • table數組中的每一個位置均可以當作一個桶,一個桶存放一個鏈表。
  • HashMap使用拉鍊法來解決衝突,同一個存放散列值相同的Node。

數據域

// 序列化ID
private static final long serialVersionUID = 362498820763181265L;  
// 初始化容量,初始化有16個桶
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16  
// 最大容量  1 073 741 824, 10億多
static final int MAXIMUM_CAPACITY = 1 << 30;
// 默認的負載因子。所以初始狀況下,當鍵值對的數量大於 16 * 0.75 = 12 時,就會觸發擴容。
static final float DEFAULT_LOAD_FACTOR = 0.75f; 
// 當put()一個元素到某個桶,其鏈表長度達到8時有可能將鏈表轉換爲紅黑樹  
static final int TREEIFY_THRESHOLD = 8;  
// 在hashMap擴容時,若是發現鏈表長度小於等於6,則會由紅黑樹從新退化爲鏈表。
static final int UNTREEIFY_THRESHOLD = 6;  
// 在轉變成紅黑樹樹以前,還會有一次判斷,只有鍵值對數量大於 64 纔會發生轉換,否者直接擴容。這是爲了不在HashMap創建初期,多個鍵值對剛好被放入了同一個鏈表中而致使沒必要要的轉化。
static final int MIN_TREEIFY_CAPACITY = 64;  
// 存儲元素的數組  
transient Node<k,v>[] table;
// 存放元素的個數
transient int size;
// 被修改的次數fast-fail機制   
transient int modCount; 
// 臨界值 當實際大小(容量*填充比)超過臨界值時,會進行擴容   
int threshold;
// 填充比
final float loadFactor;

構造函數

public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
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;
        // tableSizeFor(initialCapacity)方法計算出接近initialCapacity
        // 參數的2^n來做爲初始化容量。
        this.threshold = tableSizeFor(initialCapacity);
}
public HashMap(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
}
  • HashMap構造函數容許用戶傳入容量不是2的n次方,由於它能夠自動地將傳入的容量轉換爲2的n次方。node

    put()操做源碼解析

public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
}
static final int hash(Object key) {
        int h;
        // 「擾動函數」。參考 https://www.cnblogs.com/zhengwang/p/8136164.html
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}    
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        HashMap.Node<K,V>[] tab; HashMap.Node<K,V> p; int n, i;
        // 未初始化則初始化table
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        // 經過table的長度和hash與運算獲得一個index,
        // 而後判斷table數組下標爲index處是否已經存在node。
        if ((p = tab[i = (n - 1) & hash]) == null)
            // 若是table數組下標爲index處爲空則新建立一個node放在該處
            tab[i] = newNode(hash, key, value, null);
        else {
            // 運行到這表明table數組下標爲index處已經存在node,即發生了碰撞
            HashMap.Node<K,V> e; K k;
            // 檢查這個node的key是否跟插入的key是否相同。
            if (p.hash == hash &&
                    ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            // 檢查這個node是否已是一個紅黑樹
            else if (p instanceof TreeNode)
                // 若是這個node已是一個紅黑樹則繼續往樹種添加節點
                e = ((HashMap.TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                for (int binCount = 0; ; ++binCount) {
                    // 在這裏循環遍歷node鏈表

                    // 判斷是否到達鏈表尾
                    if ((e = p.next) == null) {
                        // 到達鏈表尾,直接把新node插入鏈表,插入鏈表尾部,在jdk8以前是頭插法
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            // 若是node鏈表的長度大於等於8則可能把這個node轉換爲紅黑樹
                            treeifyBin(tab, hash);
                        break;
                    }
                    // 檢查這個node的key是否跟插入的key是否相同。
                    if (e.hash == hash &&
                            ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            // 當插入key存在,則更新value值並返回舊value
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        // 修改次數++
        ++modCount;
        // 若是當前大小大於門限,門限本來是初始容量*0.75
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }
  • 下面簡單說下put()流程:
    1. 判斷鍵值對數組table[]是否爲空或爲null,不然以默認大小resize();
    2. 根據鍵key計算hash值與table的長度進行與運算獲得插入的數組索引 index,若是tab[index] == null,直接根據key-value新建node添加,不然轉入3
    3. 判斷當前數組中處理hash衝突的方式爲鏈表仍是紅黑樹(check第一個節點類型便可),分別處理
  • 爲啥頭插法爲何要換成尾插:jdk1.7時候用頭插法多是考慮到了一個所謂的熱點數據的點(新插入的數據可能會更早用到);找到鏈表尾部的時間複雜度是 O(n),或者須要使用額外的內存地址來保存鏈表尾部的位置,頭插法能夠節省插入耗時。可是在擴容時會改變鏈表中元素本來的順序,以致於在併發場景下致使鏈表成環的問題。
  • 從putVal()源碼能夠看出,HashMap並無對null的鍵值對作限制(hash值設爲0),即HashMap容許插入鍵尾null的鍵值對。但在JDK1.8以前HashMap使用第0個node存放鍵爲null的鍵值對。
  • 肯定node下標:經過table的長度和key的hash進行與運算獲得一個index。
  • 在轉變成紅黑樹樹以前,還會有一次判斷,只有鍵值對數量大於 64 纔會發生轉換,否者直接擴容。這是爲了不在HashMap創建初期,多個鍵值對剛好被放入了同一個鏈表中而致使沒必要要的轉化。

get()操做源碼解析

public V get(Object key) {
        HashMap.Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }
    
    final HashMap.Node<K,V> getNode(int hash, Object key) {
        HashMap.Node<K,V>[] tab; HashMap.Node<K,V> first, e; int n; K k;
        // table不爲空
        if ((tab = table) != null && (n = tab.length) > 0 &&
                // 經過table的長度和hash與運算獲得一個index,table
                // 下標位index處的元素不爲空,即元素爲node鏈表
                (first = tab[(n - 1) & hash]) != null) {
            // 首先判斷node鏈表中中第一個節點
            if (first.hash == hash && // always check first node
                    // 分別判斷key爲null和key不爲null的狀況
                    ((k = first.key) == key || (key != null && key.equals(k))))
                // key相等則返回第一個
                return first;
            // 第一個節點key不一樣且node鏈表不止包含一個節點
            if ((e = first.next) != null) {
                // 判斷node鏈表是否轉爲紅黑樹。
                if (first instanceof HashMap.TreeNode)
                    // 則在紅黑樹中進行查找。
                    return ((HashMap.TreeNode<K,V>)first).getTreeNode(hash, key);
                do {
                    // 循環遍歷node鏈表中的節點,判斷key是否相等
                    if (e.hash == hash &&
                            ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        // key在table中不存在則返回null。
        return null;
    }
  • get(key)方法首先獲取key的hash值,
    1. 計算hash & (table.len - 1)獲得在鏈表數組中的位置,
    2. 先判斷node鏈表(桶)中的第一個節點的key是否與參數key相等,
    3. 不等則判斷是否已經轉爲紅黑樹,若轉爲紅黑樹則在紅黑樹中查找,
    4. 如沒有轉爲紅黑樹就遍歷後面的鏈表找到相同的key值返回對應的Value值便可。

resize()操做源碼解析

// 初始化或者擴容以後的元素調整
    final HashMap.Node<K,V>[] resize() {
        // 獲取舊table
        HashMap.Node<K,V>[] oldTab = table;
        // 舊table容量
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        // 舊table擴容臨界值
        int oldThr = threshold;
        // 定義新table容量和臨界值
        int newCap, newThr = 0;
        // 若是原table不爲空
        if (oldCap > 0) {
            // 若是table容量達到最大值,則修改臨界值爲Integer.MAX_VALUE
            // MAXIMUM_CAPACITY = 1 << 30;
            // Integer.MAX_VALUE = 1 << 31 - 1;
            if (oldCap >= MAXIMUM_CAPACITY) {
                // Map達到最大容量,這時還要向map中放數據,則直接設置臨界值爲整數的最大值
                // 在容量沒有達到最大值以前不會再resize。
                threshold = Integer.MAX_VALUE;
                // 結束操做
                return oldTab;
            }
            // 下面就是擴容操做(2倍)
            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
            /*
             * 進入此if證實建立HashMap時用的帶參構造:public HashMap(int initialCapacity)
             * 或 public HashMap(int initialCapacity, float loadFactor)
             * 注:帶參的構造中initialCapacity(初始容量值)不論是輸入幾都會經過
             * tableSizeFor(initialCapacity)方法計算出接近initialCapacity
             * 參數的2^n來做爲初始化容量。
             * 因此實際建立的容量並不等於設置的初始容量。
             */
            newCap = oldThr;
        else {               // zero initial threshold signifies using defaults
            // 進入此if證實建立map時用的無參構造:
            // 而後將參數newCap(新的容量)、newThr(新的擴容閥界值)進行初始化
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        if (newThr == 0) {
            // 進入這表明有兩種可能。
            // 1. 說明old table容量大於0可是小於16.
            // 2. 建立HashMap時用的帶參構造,根據loadFactor計算臨界值。
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                    (int)ft : Integer.MAX_VALUE);
        }
        // 修改臨界值
        threshold = newThr;
        @SuppressWarnings({"rawtypes","unchecked"})
        // 根據新的容量生成新的 table
        HashMap.Node<K,V>[] newTab = (HashMap.Node<K,V>[])new HashMap.Node[newCap];
        // 替換成新的table
        table = newTab;
        // 若是oldTab不爲null說明是擴容,不然直接返回newTab
        if (oldTab != null) {
            /* 遍歷原來的table */
            for (int j = 0; j < oldCap; ++j) {
                HashMap.Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null;
                    // 判斷這個桶(鏈表)中就只有一個節點
                    if (e.next == null)
                        // 根據新的容量從新計算在table中的位置index,並把當前元素賦值給他。
                        newTab[e.hash & (newCap - 1)] = e;
                    // 判斷這個鏈表是否已經轉爲紅黑樹
                    else if (e instanceof HashMap.TreeNode)
                        // 在split函數中可能因爲紅黑樹的長度小於等於UNTREEIFY_THRESHOLD(6)
                        // 則把紅黑樹從新轉爲鏈表
                        ((HashMap.TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { // preserve order
                        // 運行到這裏證實桶中有多個節點。
                        HashMap.Node<K,V> loHead = null, loTail = null;
                        HashMap.Node<K,V> hiHead = null, hiTail = null;
                        HashMap.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;
    }
  • 在達到最大值MAXIMUM_CAPACITY以後仍能夠put數據。
  • 帶構造參數初始化過程當中,實際建立的容量並不等於設置的初始容量。tableSizeFor()方法能夠自動的將傳入的容量轉換2的n次方。
  • 紅黑樹能夠退化成鏈表。
  • 須要注意的是,擴容操做須要把oldTable的全部鍵值對從新插入newTable中,所以,這一步是很耗時的。
相關文章
相關標籤/搜索