1. 程式人生 > >jdk1.8 HashMap源碼講解

jdk1.8 HashMap源碼講解

abstract bool equals 最大數 得到 next 總結 fab 為什麽

1. 開篇名義  

jdk1.8中hashMap發生了一些改變,在之前的版本中hsahMap的組成是數組+鏈表的形式體現,而在1.8中則改為數組+鏈表+紅黑樹的形式實現,通過下面兩張圖來對比一下二者的不同。

技術分享圖片 技術分享圖片

jdk1.8之前的hashMap結構圖,基本對象為Entry<k,v> jdk1.8的hashMap結構圖,基本對象改為了Node<k,v>

註意:無論Entry<key,value>還是Node<key,value>都實現了 Map.Entry<K,V> 接口;

下面的內容將不在討論關於jdk1.7的內容,重點討論一下jdk1.8中hashMap的相關代碼實現

2. HashMap類概覽(實現的接口、繼承的類,以及成員變量)

public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {
    // 序列號
    private static final long serialVersionUID = 362498820763181265L;    
    // 默認的初始容量是16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 最大容量 static final int MAXIMUM_CAPACITY = 1 << 30; // 默認的填充因子0.75f,可以在創建hashMap時指定 static final float DEFAULT_LOAD_FACTOR = 0.75f; // 當桶(bucket)上的結點數大於這個值時會轉成紅黑樹 static final int TREEIFY_THRESHOLD = 8;
// 當桶(bucket)上的結點數小於這個值時樹轉鏈表 static final int UNTREEIFY_THRESHOLD = 6; // 桶中結構轉化為紅黑樹對應的table的最小長度,即:當數組的長度大於64並且桶的長度大於8同時滿足時,才會觸發由鏈表變為紅黑樹 static final int MIN_TREEIFY_CAPACITY = 64;
  /* ---------------- Fields -------------- */
   transient Node<k,v>[] table; // 存儲元素的數組,總是2的冪次倍,由tableSizeFor(int cap)方法計算得出,思考為什麽要用transient關鍵字修飾
   transient Set<map.entry<k,v>> entrySet; // 存放具體的元素的集 

transient int size; // 存放元素的個數

transient int modCount; //每次擴容和更改map結構的計數器
   int threshold; //臨界值 當實際大小(容量*填充因子)超過臨界值時,會進行擴容 
   final float loadFactor;   //hashTable的填充因子,用於實際參與運算的加載因子

}

3. HashMap構造函數

3.1 HashMap的空參構造函數

    /**
     * HashMap的空參構造函數,該函數就做了一件事:
     * 初始化轉載因子,經默認的轉載因子(0.75)賦值給loadFactor
     */
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }

3.2 HashMap的單參構造函數

    /**
     * 單參數構造函數,指定hashMap的初始數組長度值
     * 
     */
    public HashMap(int initialCapacity) {
       //調用本類中(int,float)構造函數,測試轉載因子為默認值
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

3.3 HashMap(int ,fioat)型構造函數

public HashMap(int initialCapacity, float loadFactor) {  //指定初始容量和轉載因子
    // 初始容量不能小於0,否則報錯
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " +
                                            initialCapacity);
    // 初始容量不能大於最大值(1<<30),否則為最大值
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    // 填充因子不能小於或等於0,不能為非數字
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " +
                                            loadFactor);
    // 初始化填充因子                                        
    this.loadFactor = loadFactor;
    // 初始化threshold大小,根據傳入值的大小重新計算初始hashMap數組的長度臨界值
    this.threshold = tableSizeFor(initialCapacity);    
} 

3.4 HashMap(Map<? extends K, ? extends V> m)型構造函數

        /**
         * 根據傳人的Map映射構造一個新的HashMap
         */
    public HashMap(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
    }

//putMapEntries函數會將傳進的map放到新的hashMap中
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
    int s = m.size();
    if (s > 0) {
        // 判斷table是否已經初始化
        if (table == null) { // pre-size
            // 未初始化,s為m的實際元素個數
            float ft = ((float)s / loadFactor) + 1.0F;
            int t = ((ft < (float)MAXIMUM_CAPACITY) ?
                    (int)ft : MAXIMUM_CAPACITY);
            // 計算得到的t大於閾值,則初始化閾值
            if (t > threshold)
                threshold = tableSizeFor(t);
        }
        // 已初始化,並且m元素個數大於閾值,進行擴容處理
        else if (s > threshold)
            resize();
        // 將m中的所有元素添加至HashMap中
        for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
            K key = e.getKey();
            V value = e.getValue();
            putVal(hash(key), key, value, false, evict);
        }
    }
}

3.5 總結

    • 空參構造,初始化加載因子為默認值
    • 單參數構造,指定數組初始長度(並非實際的長度,實際長度會根據tableSizeFor(int cap)函數進行計算,該構造函數自動調用雙參數構造函數
    • 雙參構造函數,指定數組的實際長度(通過計算後直接獲得)和加載因子;
    • 傳人鍵值對映射

  註意:除了最後一種構造函數調用了putVal()方法進行了數組創建之外,其他構造方法只是在維護成員屬性,並沒有實際進行創建,創建數組實在putVal()方法中執行的

4.核心方法

4.1 根據初始值計算Map數組的初始長度

    /**
     * Returns a power of two size for the given target capacity.
   * 返回大於給定值的最小的2的倍數 
*/ static final int tableSizeFor(int cap) { int n = cap - 1; n |= n >>> 1; //讓當前數據的最高位非0值補全低位數據,讓高位參數到運算中,減少hash碰撞的產生 n |= n >>> 2; n |= n >>> 4; n |= n >>> 8; n |= n >>> 16; return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; }

思考:  

  •   首先為什麽cap-1?

    這是為了防止,cap已經是2的冪。如果cap已經是2的冪, 又沒有執行這個減1操作,則執行完後面的幾條無符號右移操作之後,返回的capacity將是這個cap的2倍。

  •   為什麽是分別後移1、2、4、8 、16?

      正好是倍數關系,這樣正好能保證比原始數據小的低位全部變為1,並且保證了效率,第一次移動一位後前兩位變成11,再移動四位正好能將剛才的兩位數再降位變成1111,再移動4位    正好將1111繼續降位到後邊

  •   為什麽不繼續無符號右移32位?

      因為hashMap的最大數為1<<30,經過上面的多次移動,一共移動了1+2+4+8+16,已經超過了30位

4.2 hashMap的put()

1     public V put(K key, V value) {
2         return putVal(hash(key), key, value, false, true);    //在putVal()方法中進行第一次的數組創建
3     }

4.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;
    // 步驟①:tab為空則創建 
    // table未初始化或者長度為0,進行擴容
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    // 步驟②:計算index,並對null做處理  
    // (n - 1) & hash 確定元素存放在哪個桶中,桶為空,新生成結點放入桶中(此時,這個結點是放在數組中)
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    // 桶中已經存在元素
    else {
        Node<K,V> e; K k;
        // 步驟③:節點key存在,直接覆蓋value 
        // 比較桶中第一個元素(數組中的結點)的hash值相等,key相等
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
                // 將第一個元素賦值給e,用e來記錄
                e = p;
        // 步驟④:判斷該鏈為紅黑樹 
        // hash值不相等,即key不相等;為紅黑樹結點
        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;
                }
                // 判斷鏈表中結點的key值與插入的元素的key值是否相等
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    // 相等,跳出循環
                    break;
                // 用於遍歷桶中的鏈表,與前面的e = p.next組合,可以遍歷鏈表
                p = e;
            }
        }
        // 表示在桶中找到key值、hash值與插入元素相等的結點
        if (e != null) { 
            // 記錄e的value
            V oldValue = e.value;
            // onlyIfAbsent為false或者舊值為null
            if (!onlyIfAbsent || oldValue == null)
                //用新值替換舊值
                e.value = value;
            // 訪問後回調
            afterNodeAccess(e);
            // 返回舊值
            return oldValue;
        }
    }
    // 結構性修改
    ++modCount;
    // 步驟⑥:超過最大容量 就擴容 
    // 實際大小大於閾值則擴容
    if (++size > threshold)
        resize();
    // 插入後回調
    afterNodeInsertion(evict);
    return null;
}
  • 判斷鍵值對數組table[i]是否為空或為null,否則執行resize()進行擴容;
  • 根據鍵值key計算hash值得到插入的數組索引i,如果table[i]==null,直接新建節點添加,轉向⑥,如果table[i]不為空,轉向③;
  • 判斷table[i]的首個元素是否和key一樣,如果相同直接覆蓋value,否則轉向④,這裏的相同指的是hashCode以及equals;
  • 判斷table[i] 是否為treeNode,即table[i] 是否是紅黑樹,如果是紅黑樹,則直接在樹中插入鍵值對,否則轉向⑤;
  • 遍歷table[i],判斷鏈表長度是否大於8,大於8的話把鏈表轉換為紅黑樹,在紅黑樹中執行插入操作,否則進行鏈表的插入操作;遍歷過程中若發現key已經存在直接覆蓋value即可;
  • 插入成功後,判斷實際存在的鍵值對數量size是否超多了最大容量threshold,如果超過,進行擴容。

  ------------------未完待續------------------------------

jdk1.8 HashMap源碼講解