JDK 1.6 HashMap 原始碼分析

家裡那隻橘貓發表於2019-04-09

前言

​ 前段時間研究了一下JDK 1.6 的 HashMap 原始碼,把部份重要的方法分析一下,當然HashMap中還有一些值得研究得就交給讀者了,如有不正確之處還望留言指正。

準備

​ 需要熟悉陣列和連結串列這兩個基本資料結構。如果對連結串列不太熟悉的話,可以來幾道leetcode上的相關的連結串列演算法題。熟悉後看 HashMap 就會快很多了。 ​ 基本原理:HashMap中的基本資料結構是陣列加連結串列。table 是一個固定的陣列。 陣列裡面的每個坑裡面填的是一個叫Entry類。 其實就是一個固定的Entry陣列。如果同一個坑裡面存在兩個不同的資料,那麼兩個資料就以連結串列的形式連線起來。最新的在最前面,原因是認為最新的容易經常被訪問。

建構函式

​ 基本原理知道了。現在直接研究帶引數的建構函式就可以了,其他的建構函式就是呼叫該方法。

 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);

        // Find a power of 2 >= initialCapacity
        int capacity = 1;
        while (capacity < initialCapacity)
            capacity <<= 1;

        this.loadFactor = loadFactor;
        threshold = (int)(capacity * loadFactor);
        table = new Entry[capacity];
        init();
    }
複製程式碼

MAXIMUM_CAPACITY = 1 << 30 2的30次方1073741824,也就是HashMaptable陣列的大小不能超過該數字。 從上面程式碼可以看出來table的坑位只能是2的冪次方。如果你傳入的initialCapacity為7 那麼其實table 的大小為8; 也就是table的大小為傳入進來的initialCapacity的數值大於該大小的2的冪次方。threshold 為他的閾值也就是 HashMap 的真正大小不能超過該值,超過了就進行擴容操作。 如果table陣列的大小為16時。用它預設的擴容因子0.75f。那麼他的閾值就是12。 也就是 table資料,陣列中的加上鍊表的不能超過12。 我們看看第二個建構函式。引數為一個Map 我這裡順便把HashMap中的巢狀類Entry類說一下。可以自己再原始碼上觀看。

 public HashMap(Map<? extends K, ? extends V> m) {
 	// 對比該map的size大小,新的map最新的容量為16
        this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
                      DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
        // 建立所有map
        putAllForCreate(m);
    }


 private void putAllForCreate(Map<? extends K, ? extends V> m) {
 			// 對每一個Entry進行迭代
        for (Iterator<? extends Map.Entry<? extends K, ? extends V>> i = m.entrySet().iterator(); i.hasNext(); ) {
            Map.Entry<? extends K, ? extends V> e = i.next();
            //建立資料賦值
            putForCreate(e.getKey(), e.getValue());
        }
    }

  private void putForCreate(K key, V value) {
        int hash = (key == null) ? 0 : hash(key.hashCode());
        // 計算table中的位置
        int i = indexFor(hash, table.length);

        /**
         * Look for preexisting entry for key.  This will never happen for
         * clone or deserialize.  It will only happen for construction if the
         * input Map is a sorted map whose ordering is inconsistent w/ equals.
         */
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            // 相同的值覆蓋。
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k)))) {
                e.value = value;
                return;
            }
        }

		// 建立Entry
        createEntry(hash, key, value, i);
    }


     void createEntry(int hash, K key, V value, int bucketIndex) {
		Entry<K,V> e = table[bucketIndex];
		// 頭節點插入
        table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
        size++;
    }

// 巢狀類 和HashMap類沒關係 獨立存在 預設許可權 只能本包訪問 也就是Java.util下的包訪問 HashHap中並沒有提供 Map.Entry<K,V>這樣的返回物件出去。有的只是一個 Set<Map.Entry<K,V>>
//一個代理類。
    static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;
        V value;
        Entry<K,V> next;
        final int hash;

        /**
         * Creates new entry.
         */
        Entry(int h, K k, V v, Entry<K,V> n) {
            value = v;
            next = n;
            key = k;
            hash = h;
        }

        public final K getKey() {
            return key;
        }

        public final V getValue() {
            return value;
        }

        public final V setValue(V newValue) {
	    V oldValue = value;
            value = newValue;
            return oldValue;
        }

        public final boolean equals(Object o) {
            if (!(o instanceof Map.Entry))
                return false;
            Map.Entry e = (Map.Entry)o;
            Object k1 = getKey();
            Object k2 = e.getKey();
            if (k1 == k2 || (k1 != null && k1.equals(k2))) {
                Object v1 = getValue();
                Object v2 = e.getValue();
                if (v1 == v2 || (v1 != null && v1.equals(v2)))
                    return true;
            }
            return false;
        }

        public final int hashCode() {
            return (key==null   ? 0 : key.hashCode()) ^
                   (value==null ? 0 : value.hashCode());
        }

        public final String toString() {
            return getKey() + "=" + getValue();
        }

        /**
         * This method is invoked whenever the value in an entry is
         * overwritten by an invocation of put(k,v) for a key k that's already
         * in the HashMap.
         */
        void recordAccess(HashMap<K,V> m) {
        }

        /**
         * This method is invoked whenever the entry is
         * removed from the table.
         */
        void recordRemoval(HashMap<K,V> m) {
        }
    }
複製程式碼

put方法

​ 為什麼要從put方法研究起呢。因為HashMap中最常用得就是put方法。而且裡面還涉及到擴容操作。如果把這些看懂了還是會很舒服得。

    public V put(K key, V value) {
        if (key == null)
            // 如果key為null的話 直接新增到table[0]的位置 for 迴圈 table[0]上的元素。如果有元素的話 檢視該元素的key是不是null 如果是的話 就更新value值,直到table[0]這個連結串列結束。 如果結束後還是沒有的話,就把為null的key 對應的value 頭插法  插入頭部。 可以檢視 putForNullKey(value) 方法。
            return putForNullKey(value);

        // 計算Hash值 

        int hash = hash(key.hashCode());
        // 取key的Hash值得 二進位制數得後幾位。 如果key得hash為1101011 。而table這個陣列得大小一直都是2的冪次方。 indexFor()方法做的事 key的hash與table.length-1做&運算。假如table陣列的大小為16,也就是 11011011 & 1111 會等於 1011 。這個方法的意義也就是隻要你得Hash值是隨機的,碰撞性低,那麼你在table中位置也就是 碰撞低的。
        int i = indexFor(hash, table.length);

        // 查詢該table[i] 位置上的連結串列。
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            // 如果 key相等 那麼就更新 否則 下一位。。。。 直至結束。
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
        // 修改次數加一
        modCount++;
        // 頭插法 並看size是都大於閾值了,如果大於就要擴容了。
        addEntry(hash, key, value, i);
        return null;
    }

    void addEntry(int hash, K key, V value, int bucketIndex) {
    Entry<K,V> e = table[bucketIndex];
        table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
        if (size++ >= threshold)
            //擴容操作 2倍擴容
            resize(2 * table.length);
    }
    // 擴容方法 引數為擴容大小
    void resize(int newCapacity) {
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }
        // 建立一個新得陣列 名字叫做newTable length為 newCapacity
        Entry[] newTable = new Entry[newCapacity];
        // 擴容操作
        transfer(newTable);
        // 重新賦值 
        table = newTable;
        // 閾值
        threshold = (int)(newCapacity * loadFactor);
    }

    // 擴容操作
    void transfer(Entry[] newTable) {
        // 將原先的table陣列 賦值給 src
        Entry[] src = table;
        int newCapacity = newTable.length;
        // 逐個操作 從 src[0] 位置上的Entry 開始
        for (int j = 0; j < src.length; j++) {
            // 將src[j]的值給 e變數。
            Entry<K,V> e = src[j];
            // 對這個e 連結串列進行往下操作
            if (e != null) {
                // 清空
                src[j] = null;
                do {
                    //e 的下面一位 其實就是 next 後移 (這裡如果兩個執行緒同時在這裡操作的話,A執行緒在這裡執行這條語句後掛起的話,B執行緒完成擴容操作後,A執行緒再喚醒時,有可能發生迴圈連結串列。然後使用get方法的時候,導致死迴圈,cpu利用100%)
                    Entry<K,V> next = e.next;
                    
                    // 對e 重新定位。
                    int i = indexFor(e.hash, newCapacity);

                    // 將e.next 從e 斷開 並把e.next的值 指到 newTable[i]的值
                    e.next = newTable[i];
                    // 將 e 賦值給 newTable[i] 
                    newTable[i] = e;
                    // e 往後移
                    e = next;
                } while (e != null);
            }
        }
    }
複製程式碼

舒服了舒服了。 如果想看怎麼發生死迴圈的可以看小灰的文章 高併發下的HashMap

get方法

get方法相對而言就比較簡單了。

 public V get(Object key) {
        if (key == null)
            // 直接查詢table[0] 上鍊表key為 null的值
            return getForNullKey();
        // 定位table上的位置
        int hash = hash(key.hashCode());
        // 連結串列的查詢 
        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.equals(k)))
                return e.value;
        }
        return null;
    }
複製程式碼

remove方法

remove方法相對而言,只要你會連結串列的刪除操作,就很好理解了。如果有不明白的可以。將連結串列這個資料結構好好學習一下。

     public V remove(Object key) {
        // 移除元素方法
        Entry<K,V> e = removeEntryForKey(key);
        return (e == null ? null : e.value);
    }


    // 這裡其實就是連結串列的刪除操作 。
     final Entry<K,V> removeEntryForKey(Object key) {
        int hash = (key == null) ? 0 : hash(key.hashCode());
        // 定位位置
        int i = indexFor(hash, table.length);
        // 將table[i] 這個連結串列賦值給prev 
        Entry<K,V> prev = table[i];
        // prev 賦值給 e
        Entry<K,V> e = prev;

        while (e != null) {
            // 下面一位
            Entry<K,V> next = e.next;
            Object k;
            // key是否相等
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k)))) {
                modCount++;
                size--;
                // 如果要刪除的時table[i]的頭部資料 
                if (prev == e)
                    // table[i] 等於next 刪除頭部
                    table[i] = next;
                else
                    // 否則 刪除這個  
                    prev.next = next;
                e.recordRemoval(this);
                return e;
            }
            prev = e;
            e = next;
        }

        return e;
    }
複製程式碼

總結

HashMap中的學問,遠不止這些。 其中還涉及到設計模式,迭代器等等。上面這些只是常用的。個人非常推薦把陣列和連結串列這個兩個非常基礎的資料結構好好練習一下。雖然說早就把JDK 1.6的HashMap 原始碼看了一下,順便把 ConcurrentHashMap中的一些原始碼也看了。但是寫下來的時候,再看一遍,印象果然深刻多了。先把1.6的看了,在看1.8的吧。

相關文章