jdk1.7 HashMap源碼初探

HashMap的數據結構

HashMap有個Entry<K,V>[] table屬性用來存放最終的key-value,Entry是HashMap的內部類,table是一個Entry數組,初始是空的。只有執行第一次put的時候纔會初始化Entry大小。

新建HashMap對象

HashMap共提供了四個構造方法,其中最常用的是無參構造方法,四個構造方法分別如下:

無參構造

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

其中初始大小爲1<<4即16,擴容因子爲0.75。擴容因子用於觸發map擴容,詳見後文。

傳入一個已有的map對象

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

指定map初始大小

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

指定map初始大小和擴容因子

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;
        threshold = initialCapacity;
        init();
    }

前面三個構造方法最終都是調用最後一個構造方法,這個構造方法只是指定了容量大小(capacity),和擴容因子(loadFactor),並對入參進行合法性校驗。init方法是個空方法。此時table是空。threshold是擴容觸發值,即達到這個值就觸發擴容,詳見後文。

put操作

完整的put方法

public V put(K key, V value) {
    if (table == EMPTY_TABLE) {
        inflateTable(threshold);
    }
    if (key == null)
        return putForNullKey(value);
    int hash = hash(key);
    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;
        }
    }

    modCount++;
    addEntry(hash, key, value, i);
    return null;
}

第一次put

此時table是空的,所以需要初始化數組大小,大小取的是初始化時指定的擴容觸發值(capacity )默認16

private void inflateTable(int toSize) {
    // Find a power of 2 >= toSize
     int capacity = roundUpToPowerOf2(toSize);

     threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
     table = new Entry[capacity];
     initHashSeedAsNeeded(capacity);
 }

其中roundUpToPowerOf2方法用於控制擴容大小必須是2的乘方。初始化分如下兩步:
1.先新建一個指定大小的Entry(HashMap的內部類)
2.初始化hash掩碼值(爲後面擴容時提供判斷是否需要進行重新hash,沒有細看)

初始化之後獲取key的hash值,然後再根據hash值獲取要存放該key-value的下標

int hash = hash(key);
int i = indexFor(hash, table.length);

如果key是null,會調用putForNullKey方法插入key爲null的Entry。所以HashMap的key是可以爲null的

private V putForNullKey(V value) {
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {
            if (e.key == null) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
        modCount++;
        addEntry(0, null, value, 0);
        return null;
    }

然後調用addEntry方法插入數據(由於是第一次put,所以Entry<K,V> e = table[i] 是null,不會進入for操作)

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;
         bucketIndex = indexFor(hash, table.length);
     }

     createEntry(hash, key, value, bucketIndex);
 }

由於第一次put,無需擴容,所有直接調用createEntry創建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++;
}

說白了就是new了一個Entry對象並放入下標爲剛纔獲取的下標下,看下Entry的構造函數

final K key;
V value;
Entry<K,V> next;
int hash;
Entry(int h, K k, V v, Entry<K,V> n) {
   value = v;
    next = n;
    key = k;
    hash = h;
}

很簡單,就四個屬性,除了key,value外,還有一個hash值,以及它的nextEntry,說明這是一個鏈表結構。
至此第一個元素就已經put好了

無需擴容

大致同第一次put,只是少了一開始的初始化數組大小的操作。另外,如果這時put的key的hash值後獲取到的下標所在元素已經存在了,則需要進入剛纔沒有進去的for循環進行替換操作

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

這段主要是判斷key是否存在,如果存在則替換value值。判斷依據是1.是否是同一個對象,2.調用key的equals方法,返回是否爲true。
如果key不存在,則繼續調用createEntry添加entry。如果該小標已經存在對象,則將原來存在的Entry放到自己Entry的next的屬性裏,即插入了鏈表的頭部(頭插法)。

需要擴容

大致同上,主要區別在於addEntry方法裏的if語句塊裏

 if ((size >= threshold) && (null != table[bucketIndex])) {
    resize(2 * table.length);
     hash = (null != key) ? hash(key) : 0;
     bucketIndex = indexFor(hash, table.length);
 }

由此可見,HashMap擴容的觸發條件是,當前已經插入的對象數量超過了需要擴容的數量(threshold),且要插入的下標有值。我之前會疑惑如果別的元素都插了好多了就這個下標沒元素,這樣豈不很不科學?其實key的hash值是能夠保證正態分佈的,無須擔心。擴容完成後需要對key進行重新hash並獲取新的下標(畢竟數組大小變了),其餘同上。下面詳細介紹擴容邏輯

擴容

resize方法

void resize(int newCapacity) {
   Entry[] oldTable = table;
    int oldCapacity = oldTable.length;
    if (oldCapacity == MAXIMUM_CAPACITY) {
        threshold = Integer.MAX_VALUE;
        return;
    }

    Entry[] newTable = new Entry[newCapacity];
    transfer(newTable, initHashSeedAsNeeded(newCapacity));
    table = newTable;
    threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}

resize大致有如下幾個步驟:
1.首先判斷已有的table數組大小有沒有超過int的最大值(2^31),所以HashMap是不能無限制存東西的。
2.然後再新建一個新的大小的數組(原有大小的兩倍)。說白了就是擴大了一倍。
3.然後調用transfer方法對已有的Entry數組進行重新hash(因爲數組大小不一樣了,所以同一個key的hash值也不一樣了,所以要重新hash,不然就get不到了)
4.最後將重新散列後的entry數組賦給table,並重新修改擴容觸發閾值
接下來詳細說下transfer方法:

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

transfer方法也很簡單,遍歷table中的每個Entry,然後再遍歷每個Entry的鏈表。然後對鏈表上的每一個Entry的key進行重新hash,存入新的table中。

get操作

final Entry<K,V> getEntry(Object key) {
        if (size == 0) {
            return null;
        }

        int hash = (key == null) ? 0 : hash(key);
        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;
        }
        return null;
    }

根據key的hash值獲取table下標,然後再遍歷該鏈表的每一個Entry,如果key相同則返回。(極端情況下,如果鏈表太長,會影響get的效率,畢竟遍歷鏈表用的for循環啊。所以要設置合理的擴容因子,默認的0.75一般還是不要改的好)

jdk1.7擴容造成死循環分析

假設一個map有如下幾個Entry
原有的Entry
擴容之後table數組大小變成4,兩個線程同時執行擴容操作,線程一執行完transfer方法的e.next地方時被掛起,此時next是key7
在這裏插入圖片描述
此時線程二執行完整個擴容操作,擴容完成後的table結構如下
在這裏插入圖片描述
線程一繼續執行,此時e是key:3,next=e.next=null(因爲線程2已經完成了重新hash,將key3的next置爲null,如圖)。
線程1開始運行,繼續進行擴容
1.此時e是key3,next是key7(剛纔已經執行到這然後被掛起的)。線程1新建的newTable與線程2新建的newTable不是同一個對象,所以此時線程1新建的newTable下標爲3的元素還是空的。繼續下面的邏輯,將key3頭插到下標爲3的table中,key3的next爲空(因爲此時newTable下標爲3的元素還是空),由於next是key7不是null,繼續循環。
2.此時e是key7,由於線程2已經把key7的next置爲了key3,所以此時next是key3,繼續下面的邏輯,將key7頭插到小標爲3的table中,key7的next被線程1又置一次爲key3,由於next是key3不爲空,繼續循環。
3.此時e是key3,next爲空(見第一步),將key3頭插到下標爲3的元素中,key3的next被置爲key7.但此時的next是null,所以循環結束,此時線程1的tabla結構如下

在這裏插入圖片描述
當get一個key時,如果該key被hash到下標爲3這個元素,且不存在於map中,比如key15,程序就會遍歷下標爲3的Entry鏈表,先找到key3,然後遍歷key3的next key7,然後再找key7的next key3,由於一直找不到key15,所以就會一直循環下去,就陷入了死循環。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章