JDK1.7中HashMap底層實現原理

1、數據結構

HashMap中的數據結構是數組+單鏈表的組合,以鍵值對(key-value)的形式存儲元素的,經過put()和get()方法儲存和獲取對象。java

(方塊表示Entry對象,橫排表示數組table[],縱排表示哈希桶bucket【其實是一個由Entry組成的鏈表,新加入的Entry放在鏈頭,最早加入的放在鏈尾】,)算法

2、實現原理

成員變量

源碼分析:數組

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

    /** 最大初始容量,2^30 */
    static final int MAXIMUM_CAPACITY = 1 << 30;

    /** 負載因子,默認0.75,負載因子越小,hash衝突機率越低 */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    /** 初始化一個Entry的空數組 */
    static final Entry<?,?>[] EMPTY_TABLE = {};

    /** 將初始化好的空數組賦值給table,table數組是HashMap實際存儲數據的地方,並不在EMPTY_TABLE數組中 */
    transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;

    /** HashMap實際存儲的元素個數 */
    transient int size;

    /** 臨界值(HashMap 實際能存儲的大小),公式爲(threshold = capacity * loadFactor) */
    int threshold;

    /** 負載因子 */
    final float loadFactor;

    /** HashMap的結構被修改的次數,用於迭代器 */
    transient int modCount;

構造方法

源碼分析:安全

    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);
        // 設置負載因子,臨界值此時爲容量大小,後面第一次put時由inflateTable(int toSize)方法計算設置
        this.loadFactor = loadFactor;
        threshold = initialCapacity;
        init();
    }

    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

    public HashMap() {
        this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
    }

    public HashMap(Map<? extends K, ? extends V> m) {
        this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
                      DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
        inflateTable(threshold);
        putAllForCreate(m);
    }

put方法

put()源碼分析:數據結構

public V put(K key, V value) {  
    // 若是table引用指向成員變量EMPTY_TABLE,那麼初始化HashMap(設置容量、臨界值,新的Entry數組引用)
    if (table == EMPTY_TABLE) {
        inflateTable(threshold);
    }
    // 若「key爲null」,則將該鍵值對添加到table[0]處,遍歷該鏈表,若是有key爲null,則將value替換。沒有就建立新Entry對象放在鏈表表頭
    // 因此table[0]的位置上,永遠最多存儲1個Entry對象,造成不了鏈表。key爲null的Entry存在這裏 
    if (key == null)  
        return putForNullKey(value);  
    // 若「key不爲null」,則計算該key的哈希值
    int hash = hash(key);  
    // 搜索指定hash值在對應table中的索引
    int i = indexFor(hash, table.length);  
    // 循環遍歷table數組上的Entry對象,判斷該位置上key是否已存在
    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))) {  
            // 若是這個key對應的鍵值對已經存在,就用新的value代替老的value,而後退出!
            V oldValue = e.value;  
            e.value = value;  
            e.recordAccess(this);  
            return oldValue;  
        }  
    }  
    // 修改次數+1
    modCount++;
    // table數組中沒有key對應的鍵值對,就將key-value添加到table[i]處 
    addEntry(hash, key, value, i);  
    return null;  
}  

能夠看到,當咱們給put()方法傳遞鍵和值時,HashMap會由key來調用hash()方法,返回鍵的hash值,計算Index後用於找到bucket(哈希桶)的位置來儲存Entry對象。多線程

若是兩個對象key的hash值相同,那麼它們的bucket位置也相同,但equals()不相同,添加元素時會發生hash碰撞,也叫hash衝突,HashMap使用鏈表來解決碰撞問題。併發

分析源碼可知,put()時,HashMap會先遍歷table數組,用hash值和equals()判斷數組中是否存在徹底相同的key對象, 若是這個key對象在table數組中已經存在,就用新的value代替老的value。若是不存在,就建立一個新的Entry對象添加到table[ i ]處。源碼分析

若是該table[ i ]已經存在其餘元素,那麼新Entry對象將會儲存在bucket鏈表的表頭,經過next指向原有的Entry對象,造成鏈表結構(hash碰撞解決方案)。性能

Entry數據結構源碼以下(HashMap內部類):測試

 static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;
        V value;
        /** 指向下一個元素的引用 */
        Entry<K,V> next;
        int hash;

        /**
         * 構造方法爲Entry賦值
         */
        Entry(int h, K k, V v, Entry<K,V> n) {
            value = v;
            next = n;
            key = k;
            hash = h;
        }
        ...
        ...
 } 

造成單鏈表的核心代碼以下:

    /**
     * 將Entry添加到數組bucketIndex位置對應的哈希桶中,並判斷數組是否須要擴容
     */
    void addEntry(int hash, K key, V value, int bucketIndex) {
        // 若是數組長度大於等於容量×負載因子,而且要添加的位置爲null
        if ((size >= threshold) && (null != table[bucketIndex])) {
            // 長度擴大爲原數組的兩倍,代碼分析見下面擴容機制
            resize(2 * table.length);
            hash = (null != key) ? hash(key) : 0;
            bucketIndex = indexFor(hash, table.length);
        }
        createEntry(hash, key, value, bucketIndex);
    }

    /**
     * 在鏈表中添加一個新的Entry對象在鏈表的表頭
     */
    void createEntry(int hash, K key, V value, int bucketIndex) {
        Entry<K,V> e = table[bucketIndex];
        table[bucketIndex] = new Entry<>(hash, key, value, e);
        size++;
    }

(put方法執行過程)

get方法

若是兩個不一樣的key的hashcode相同,兩個值對象儲存在同一個bucket位置,要獲取value,咱們調用get()方法,HashMap會使用key的hashcode找到bucket位置,由於HashMap在鏈表中存儲的是Entry鍵值對,因此找到bucket位置以後,會調用key的equals()方法,按順序遍歷鏈表的每一個 Entry,直到找到想獲取的 Entry 爲止——若是剛好要搜索的 Entry 位於該 Entry 鏈的最末端(該 Entry 是最先放入該 bucket 中),那HashMap必須循環到最後才能找到該元素。

get()方法源碼以下:

    public V get(Object key) {
        // 若key爲null,遍歷table[0]處的鏈表(實際上要麼沒有元素,要麼只有一個Entry對象),取出key爲null的value
        if (key == null)
            return getForNullKey();
        // 若key不爲null,用key獲取Entry對象
        Entry<K,V> entry = getEntry(key);
        // 若鏈表中找到的Entry不爲null,返回該Entry中的value
        return null == entry ? null : entry.getValue();
    }

    final Entry<K,V> getEntry(Object key) {
        if (size == 0) {
            return null;
        }
        // 計算key的hash值
        int hash = (key == null) ? 0 : hash(key);
        // 計算key在數組中對應位置,遍歷該位置的鏈表
        for (Entry<K,V> e = table[indexFor(hash, table.length)];
             e != null;
             e = e.next) {
            Object k;
            // 若key徹底相同,返回鏈表中對應的Entry對象
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k))))
                return e;
        }
        // 鏈表中沒找到對應的key,返回null
        return null;
    }

3、hash算法

咱們能夠看到在HashMap中要找到某個元素,須要根據key的hash值來求得對應數組中的位置。如何計算這個位置就是hash算法。前面說過HashMap的數據結構是數組和鏈表的結合,因此咱們固然但願這個HashMap裏面的元素位置儘可能的分佈均勻些,儘可能使得每一個位置上的元素數量只有一個,那麼當咱們用hash算法求得這個位置的時候,立刻就能夠知道對應位置的元素就是咱們要的,而不用再去遍歷鏈表。 

源碼分析:

    /**
     * Returns index for hash code h.
     */
    static int indexFor(int h, int length) {
        // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
        return h & (length-1);
    }

4、性能問題

HashMap有兩個參數影響其性能:初始容量和負載因子。都可以經過構造方法指定大小。

容量capacity是HashMap中bucket哈希桶(Entry的鏈表)的數量,初始容量只是HashMap在建立時的容量,最大設置初始容量是2^30,默認初始容量是16(必須爲2的冪),解釋一下,當數組長度爲2的n次冪的時候,不一樣的key經過indexFor()方法算得的數組位置相同的概率較小,那麼數據在數組上分佈就比較均勻,也就是說碰撞的概率小,相對的,get()的時候就不用遍歷某個位置上的鏈表,這樣查詢效率也就較高了。

負載因子loadFactor是HashMap在其容量自動增長以前能夠達到多滿的一種尺度,默認值是0.75。

擴容機制:

當HashMapde的長度超出了加載因子與當前容量的乘積(默認16*0.75=12)時,經過調用resize方法從新建立一個原來HashMap大小的兩倍newTable數組,最大擴容到2^30+1,並將原先table的元素所有移到newTable裏面,從新計算hash,而後再從新根據hash分配位置。這個過程叫做rehash,由於它調用hash方法找到新的bucket位置。

擴容機制源碼分析:

    void resize(int newCapacity) {
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        // 若是以前的HashMap已經擴充打最大了,那麼就將臨界值threshold設置爲最大的int值
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }

        // 根據新傳入的newCapacity建立新Entry數組
        Entry[] newTable = new Entry[newCapacity];
        // 用來將原先table的元素所有移到newTable裏面,從新計算hash,而後再從新根據hash分配位置
        transfer(newTable, initHashSeedAsNeeded(newCapacity));
        // 再將newTable賦值給table
        table = newTable;
        // 從新計算臨界值,擴容公式在這兒(newCapacity * loadFactor)
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }
    void transfer(Entry[] newTable, boolean rehash) {
        int newCapacity = newTable.length;
        for (Entry<K,V> e : table) {
            while(null != e) {
                Entry<K,V> next = e.next;
                if (rehash) {
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
                int i = indexFor(e.hash, newCapacity);
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            }
        }
    }

擴容問題:

數組擴容以後,最消耗性能的點就出現了:原數組中的數據必須從新計算其在新數組中的位置,並放進去,這個操做是極其消耗性能的。因此若是咱們已經預知HashMap中元素的個數,那麼預設初始容量可以有效的提升HashMap的性能。

從新調整HashMap大小,當多線程的狀況下可能產生條件競爭。由於若是兩個線程都發現HashMap須要從新調整大小了,它們會同時試着調整大小。在調整大小的過程當中,存儲在鏈表中的元素的次序會反過來,由於移動到新的bucket位置的時候,HashMap並不會將元素放在鏈表的尾部,而是放在頭部,這是爲了不尾部遍歷(tail traversing)。若是條件競爭發生了,那麼就死循環了。

 5、線程安全

HashMap是線程不安全的,在多線程狀況下直接使用HashMap會出現一些莫名其妙不可預知的問題。在多線程下使用HashMap,有幾種方案:

A.在外部包裝HashMap,實現同步機制

B.使用Map m = Collections.synchronizedMap(new HashMap(...));實現同步(官方參考方案,但不建議使用,使用迭代器遍歷的時候修改映射結構容易出錯)

D.使用java.util.HashTable,效率最低(幾乎被淘汰了)

E.使用java.util.concurrent.ConcurrentHashMap,相對安全,效率高(建議使用)

注意一個小問題,HashMap全部集合類視圖所返回迭代器都是快速失敗的(fail-fast),在迭代器建立以後,若是從結構上對映射進行修改,除非經過迭代器自身的 remove 或 add 方法,其餘任什麼時候間任何方式的修改,迭代器都將拋出 ConcurrentModificationException。。所以,面對併發的修改,迭代器很快就會徹底失敗。

6、關於JDK1.8的問題

JDK1.8的HashMap源碼實現和1.7是不同的,有很大不一樣,其底層數據結構也不同,引入了紅黑樹結構。有網友測試過,JDK1.8HashMap的性能要高於JDK1.7 15%以上,在某些size的區域上,甚至高於100%。隨着size的變大,JDK1.7的花費時間是增加的趨勢,而JDK1.8是明顯的下降趨勢,而且呈現對數增加穩定。當一個鏈表長度大於8的時候,HashMap會動態的將它替換成一個紅黑樹(JDK1.8引入紅黑樹大程度優化了HashMap的性能),這會將時間複雜度從O(n)降爲O(logn)。

相關文章
相關標籤/搜索