1. 程式人生 > 其它 >【JDK原始碼】HashMap原始碼分析

【JDK原始碼】HashMap原始碼分析

技術標籤:程式碼狂魔hashmapjava資料結構

Map 這樣的 Key Value 在軟體開發中是非常經典的結構,常用於在記憶體中存放資料。

總結

JDK1.7

  • 陣列加連結串列,"拉鍊法"解決hash衝突
  • 底層陣列長度總是為2的冪次方。這是因為在此條件下hash & (length - 1) == hash % length,而且&比%的效率更高,(hash % length總是小於length的,因此可以用來計算元素在桶中的位置)
    • 預設長度16,會動態增長為32,64,128…,就算初始化的時候指定為11,其實底層的陣列長度還是16
  • 負載因子預設是0.75,是可以修改的,擴容閾值=陣列長度*負載因子
    • 假設陣列長度為16,則擴容閾值為16*0.75=12,當實際所放元素大於12時,則觸發擴容操作
  • 自動擴容,非常消耗效能
  • 當hash嚴重衝突時,連結串列會越來越長嚴重影響效率,時間複雜度最長為O(N)
  • 執行緒不安全(需要執行緒安全的請使用ConcurrentHashMap),多執行緒會引發連結串列死迴圈

JDK1.8後的優化

  • 當連結串列長度超過8的時候則直接轉換成紅黑樹,查詢效率為O(logN)
  • 擴容時會均勻分配元素,而JDK1.7會原封不動的拷貝過來
  • 多執行緒會引發連結串列死迴圈的問題已解決

測試

//指定初始容量為11,但是底層陣列的長度還是會初始化為16,具體看tableSizeFor方法
Map<Integer,String> map = new HashMap(11);


//初始化map
map = new HashMap<>();//初始化容量為16的HashMap map.put(1,"A");//索引位置 1 % 16 = 1;放入第一個元素的時候會初始化map //元素放在不同的桶中 map = new HashMap<>();//初始化容量為16的HashMap map.put(1,"A");//索引位置 1 % 16 = 1;放入第一個元素的時候會初始化map map.put(2,"B");//索引位置 2 % 16 = 2 //hash碰撞,產生連結串列 map = new HashMap<>
();//初始化容量為16的HashMap map.put(1,"A");//索引位置 1 % 16 = 1;放入第一個元素的時候會初始化map map.put(17,"B");//索引位置 17 % 16 = 1,索引相同,hash碰撞,產生連結串列 //hash碰撞,產生紅黑樹 map = new HashMap<>();//初始化容量為16的HashMap map.put(1,"A"); //索引位置 1 % 16 = 1;放入第一個元素的時候會初始化map map.put(17,"B"); //索引位置 17 % 16 = 1,hash碰撞,連結串列長度2 map.put(33,"D"); //索引位置 33 % 16 = 1,hash碰撞,連結串列長度3 map.put(49,"E"); //索引位置 49 % 16 = 1,hash碰撞,連結串列長度4 map.put(65,"F"); //索引位置 65 % 16 = 1,hash碰撞,連結串列長度5 map.put(81,"G"); //索引位置 81 % 16 = 1,hash碰撞,連結串列長度6 map.put(97,"H"); //索引位置 97 % 16 = 1,hash碰撞,連結串列長度7 map.put(113,"I"); //索引位置 113 % 16 = 1,hash碰撞,連結串列長度8 map.put(129,"J"); //索引位置 129 % 16 = 1,hash碰撞,連結串列長度9,此時產生紅黑樹,呼叫treeifyBin(tab, hash)

JDK1.8

成員變數

/**
 * The default initial capacity - MUST be a power of two.
 */
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
  • DEFAULT_INITIAL_CAPACITY表示初始化容量大小為2^4 = 16,可以在初始化的時候指定
/**
 * The maximum capacity, used if a higher value is implicitly specified
 * by either of the constructors with arguments.
 * MUST be a power of two <= 1<<30.
 */
static final int MAXIMUM_CAPACITY = 1 << 30;
  • 最大容量為2^30
/**
 * The load factor used when none specified in constructor.
 */
static final float DEFAULT_LOAD_FACTOR = 0.75f;
  • 負載因子預設為0.75
/**
 * The bin count threshold for using a tree rather than list for a
 * bin.  Bins are converted to trees when adding an element to a
 * bin with at least this many nodes. The value must be greater
 * than 2 and should be at least 8 to mesh with assumptions in
 * tree removal about conversion back to plain bins upon
 * shrinkage.
 */
static final int TREEIFY_THRESHOLD = 8;

  • 當連結串列長度大於8的時候,連結串列將轉換成紅黑樹
/**
 * 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.)
 */
transient Node<K,V>[] table;

  • table是真正存放資料的陣列
/**
* The number of key-value mappings contained in this map.
*/
transient int size;

  • size表示當前map實際存放元素數量的大小
/**
 * The number of times this HashMap has been structurally modified
 * Structural modifications are those that change the number of mappings in
 * the HashMap or otherwise modify its internal structure (e.g.,
 * rehash).  This field is used to make iterators on Collection-views of
 * the HashMap fail-fast.  (See ConcurrentModificationException).
 */
transient int modCount;
  • 結構化修改次數的大小
/**
 * The next size value at which to resize (capacity * load factor).
 *
 * @serial
 */
int threshold;

  • 需要擴容的閾值,當size和threshold相等時會觸發擴容操作
/**
* The load factor for the hash table.
*
* @serial
*/
final float loadFactor;

  • 負載因子,預設為DEFAULT_LOAD_FACTOR,可構造map的時候傳入

容量capacity©,負載因子loadFactor(L),擴容閾值threshold(T)和實際存放元素大小size(S)的關係

  • 注意capacity變數不是成員變數,而是實際存放資料陣列的長度,可以理解成table.length
  • T = C * L
  • 當S>T時會觸發擴容操作,此時C會變成原來的2倍(當陣列長度C 是2的 n 次時, hash&(length-1) == hash%length,因為&操作比%操作效率更搞,所以陣列長度C總是2的n次方,目的是為了提升效率。)
    舉例:
    預設大小C為16,此時T=16 * 0.75 = 12,當S=13時,觸發擴容,C=C2=162=32,T=32 * 0.75=24

put方法

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
    //定義tab p n i          
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    
    //第一次往map裡面put時候會呼叫擴容resize方法初始化table
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;//初始化table並且將長度賦值給n
    
    //i = (n - 1) & hash 會得到當前元素放置在陣列中的位置,
    //和hash % n的值相等(前提是table.length為2的冪次方),但是&的操作效率更高
    //如果該位置上面沒有元素則直接新建一個元素
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
      
    //下面的操作在該元素位置上有值的時候進行操作    
    else {
        Node<K,V> e; K k;
        //如果當前桶有值( Hash 衝突),那麼就要比較當前桶中的 key、key 的 hashcode 與寫入的 key 是否相等,相等就賦值給 e
        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;
        }
    }
    //結構化修改加1
    ++modCount;
    
    //如果實際所裝的元素大於了閾值,則觸發擴容操作
    if (++size > threshold)
        resize();
        
    afterNodeInsertion(evict);
    return null;
}

get方法

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;
    
    //first = tab[(n - 1) & hash] 通過(n - 1) & hash定位到該鍵所在桶的索引
    //如果桶為null則直接返回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);
        }
    }
    //沒找到或各種意外情況下都返回null
    return null;
}

JDK1.7

JDK1.7中的put和get方法就簡單很多,也沒有紅黑樹那些轉換

put

public V put(K key, V value) {
    //沒有初始化的時候先初始化
    if (table == EMPTY_TABLE) {
        inflateTable(threshold);
    }
    //空key的時候
    if (key == null)
        return putForNullKey(value);
    
    //計算hash    
    int hash = hash(key);
    //計算出該hash在當前桶中的定位
    int i = indexFor(hash, table.length);
    
    //如果桶是一個連結串列,則迴圈連結串列
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {
        Object k;
        //如果在當前連結串列中找到鍵相同的,則替換掉原來的值
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            //返回原來的值
            return oldValue;
        }
    }
    //如果桶是空的或者沒在連結串列中找到一樣的鍵則新增加一個Entry
    modCount++;
    addEntry(hash, key, value, i);
    return null;
}


void addEntry(int hash, K key, V value, int bucketIndex) {
    //如果實際的容量達到了閾值,則需要擴容
    if ((size >= threshold) && (null != table[bucketIndex])) {
        //擴容為原來的兩倍
        resize(2 * table.length);
        hash = (null != key) ? hash(key) : 0;
        //在計算出該key的索引,即在桶中的位置
        bucketIndex = indexFor(hash, table.length);
    }
    //建立Entry
    createEntry(hash, key, value, bucketIndex);
}


void createEntry(int hash, K key, V value, int bucketIndex) {
    //把當前位置上的元素拿出來
    Entry<K,V> e = table[bucketIndex];
    //新建Entry,如果e不是null的話,則形成連結串列結構
    table[bucketIndex] = new Entry<>(hash, key, value, e);
    size++;
}

get

public V get(Object key) {
    if (key == null)
        return getForNullKey();
    Entry<K,V> entry = getEntry(key);
    return null == entry ? null : entry.getValue();
}


final Entry<K,V> getEntry(Object key) {
    if (size == 0) {
        return null;
    }
    //計算hashCode
    int hash = (key == null) ? 0 : hash(key);
    
    //indexFor(hash, table.length) 計算出在桶中的位置
    //如果是連結串列則迴圈找到鍵相同的Entry
    for (Entry<K,V> e = table[indexFor(hash, table.length)];
         e != null;
         e = e.next) {
        Object k;
        if (e.hash == hash &&
            ((k = e.key) == key || (key != null && key.equals(k))))
            return e;
    }
    
    //如果啥也沒找到,則直接返回null
    return null;
}