HashMap爲什麼從頭插入改成尾插入

微信公衆號:I am CR7
若有問題或建議,請在下方留言;
最近更新:2018-09-21php

前言

     前面對於HashMap在jdk1.8中元素插入的實現原理,進行了詳細分析,具體請看:HashMap之元素插入。文章發佈以後,有一位朋友問了這麼一個問題:"jdk1.7中採用頭插入,爲何jdk1.8中改爲了尾插入?"。有人說這就是java大神隨性而爲,沒什麼特殊的用處。當時由於沒仔細看過1.7的源碼,因此很差解答。如今特此寫了本文,來對該問題進行詳細的分析。java

靜態常量

源碼:
 1/**
2 * 默認初始大小,值爲16,要求必須爲2的冪
3 */

4static final int DEFAULT_INITIAL_CAPACITY = 1 << 4// aka 16
5
6/**
7 * 最大容量,必須不大於2^30
8 */

9static final int MAXIMUM_CAPACITY = 1 << 30;
10
11/**
12 * 默認加載因子,值爲0.75
13 */

14static final float DEFAULT_LOAD_FACTOR = 0.75f;
15
16/**
17 * HashMap的空數組
18 */

19static final Entry<?,?>[] EMPTY_TABLE = {};
20
21/**
22 * 可選的默認哈希閾值
23 */

24static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;
複製代碼

     注意:jdk1.7中HashMap默認採用數組+單鏈表方式存儲元素,當元素出現哈希衝突時,會存儲到該位置的單鏈表中。這和1.8不一樣,除了數組和單鏈表外,當單鏈表中元素個數超過8個時,會進而轉化爲紅黑樹存儲,巧妙地將遍歷元素時時間複雜度從O(n)下降到了O(logn))。算法

構造函數

一、無參構造函數:

1public HashMap() {
2    this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
3}
複製代碼

二、帶參構造函數,指定初始容量:

1public HashMap(int initialCapacity) {
2    this(initialCapacity, DEFAULT_LOAD_FACTOR);
3}
複製代碼

三、帶參構造函數,指定初始容量和加載因子:

 1public HashMap(int initialCapacity, float loadFactor) {
2    if (initialCapacity < 0)
3        throw new IllegalArgumentException("Illegal initial capacity: " +
4                                           initialCapacity);
5    if (initialCapacity > MAXIMUM_CAPACITY)
6        initialCapacity = MAXIMUM_CAPACITY;
7    if (loadFactor <= 0 || Float.isNaN(loadFactor))
8        throw new IllegalArgumentException("Illegal load factor: " +
9                                           loadFactor);
10
11    this.loadFactor = loadFactor;
12    threshold = initialCapacity;//和jdk8不一樣,初始閾值就是初始容量,並沒作2次冪處理
13    init();
14}
複製代碼
四、帶參構造函數,指定Map集合:
 1public void putAll(Map<? extends K, ? extends V> m) {
2        int numKeysToBeAdded = m.size();
3        if (numKeysToBeAdded == 0)
4            return;
5
6        if (table == EMPTY_TABLE) {
7            inflateTable((int) Math.max(numKeysToBeAdded * loadFactor, threshold));
8        }
9
10        if (numKeysToBeAdded > threshold) {
11            int targetCapacity = (int)(numKeysToBeAdded / loadFactor + 1);
12            if (targetCapacity > MAXIMUM_CAPACITY)
13                targetCapacity = MAXIMUM_CAPACITY;
14            int newCapacity = table.length;
15            while (newCapacity < targetCapacity)
16                newCapacity <<= 1;
17            if (newCapacity > table.length)
18                resize(newCapacity);
19        }
20
21        for (Map.Entry<? extends K, ? extends V> e : m.entrySet())
22            put(e.getKey(), e.getValue());
23    }
複製代碼

     說明:執行構造函數時,存儲元素的數組並不會進行初始化,而是在第一次放入元素的時候,纔會進行初始化操做。建立HashMap對象時,僅僅計算初始容量和新增閾值。數組

添加元素

一、源碼:

 1public V put(K key, V value) {
2    if (table == EMPTY_TABLE) {
3        inflateTable(threshold);//初始化數組
4    }
5    if (key == null)//key爲null,作key爲null的添加
6        return putForNullKey(value);
7    int hash = hash(key);//計算鍵值的哈希
8    int i = indexFor(hash, table.length);//根據哈希值獲取在數組中的索引位置
9    for (Entry<K,V> e = table[i]; e != null; e = e.next) {//遍歷索引位置的單鏈表,判斷是否存在指定key
10        Object k;
11        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {//key已存在則更新value值
12            V oldValue = e.value;
13            e.value = value;
14            e.recordAccess(this);
15            return oldValue;
16        }
17    }
18
19    modCount++;
20    addEntry(hash, key, value, i);//key不存在,則插入元素
21    return null;
22}
23
24private V putForNullKey(V value) {
25    for (Entry<K,V> e = table[0]; e != null; e = e.next) {
26        if (e.key == null) {//key爲null已存在,更新value值
27            V oldValue = e.value;
28            e.value = value;
29            e.recordAccess(this);
30            return oldValue;
31        }
32    }
33    modCount++;
34    addEntry(0null, value, 0);//不存在則新增,key爲null的哈希值爲0
35    return null;
36}
37
38void addEntry(int hash, K key, V value, int bucketIndex) {
39    if ((size >= threshold) && (null != table[bucketIndex])) {//插入位置存在元素,而且元素個數大於等於新增閾值
40        resize(2 * table.length);//進行2倍擴容
41        hash = (null != key) ? hash(key) : 0;//擴容中可能會調整哈希種子的值,因此從新計算哈希值
42        bucketIndex = indexFor(hash, table.length);//從新計算在擴容後數組中的位置
43    }
44
45    createEntry(hash, key, value, bucketIndex);//添加元素
46}
47
48//計算對象哈希值
49final int hash(Object k) {
50    int h = hashSeed;
51    if (0 != h && k instanceof String) {//String採用單獨的算法
52        return sun.misc.Hashing.stringHash32((String) k);
53    }
54
55    h ^= k.hashCode();//利用哈希種子異或哈希值,爲了進行優化,增長隨機性
56
57    h ^= (h >>> 20) ^ (h >>> 12);
58    return h ^ (h >>> 7) ^ (h >>> 4);//這裏的移位異或操做屬於擾亂函數,都是爲了增長哈希值的隨機性,下降哈希衝突的機率
59}
60
61void createEntry(int hash, K key, V value, int bucketIndex) {
62    Entry<K,V> e = table[bucketIndex];
63    table[bucketIndex] = new Entry<>(hash, key, value, e);//新增元素插入到數組索引位置,原來元素做爲其後繼節點,即採用頭插入方法
64    size++;
65}
複製代碼

二、流程圖:

圖注:添加元素流程圖
圖注:添加元素流程圖

三、示例:

圖注:初始狀態
圖注:初始狀態

圖注:添加10
圖注:添加10

圖注:添加18
圖注:添加18

圖注:擴容
圖注:擴容

圖注:擴容後添加
圖注:擴容後添加

初始化數組

一、源碼:

 1//根據指定的大小,初始化數組
2private void inflateTable(int toSize) {
3    // Find a power of 2 >= toSize
4    int capacity = roundUpToPowerOf2(toSize);
5
6    threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);//根據容量和加載因子計算閾值,最大爲2^30+1
7    table = new Entry[capacity];//建立指定容量大小的數組
8    initHashSeedAsNeeded(capacity);
9}
10
11//獲取大於指定值的最小2次冪,最大爲2^30
12private static int roundUpToPowerOf2(int number) {
13    // assert number >= 0 : "number must be non-negative";
14    return number >= MAXIMUM_CAPACITY
15            ? MAXIMUM_CAPACITY
16            : (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
17}
複製代碼

二、說明:

     關於哈希種子,是爲了優化哈希函數,讓其值更加隨機,從而下降哈希衝突的機率。經過HashMap中私有靜態類Holder,在JVM啓動的時候,指定-Djdk.map.althashing.threshold=值,來設置可選的哈希閾值,從而在initHashSeedAsNeeded中決定是否須要調整哈希種子。微信

 1private static class Holder {
2
3    /**
4     * Table capacity above which to switch to use alternative hashing.
5     */

6    static final int ALTERNATIVE_HASHING_THRESHOLD;
7
8    static {
9        String altThreshold = java.security.AccessController.doPrivileged(
10            new sun.security.action.GetPropertyAction(
11                "jdk.map.althashing.threshold"));//經過-Djdk.map.althashing.threshold=值指定可選哈希閾值
12
13        int threshold;
14        try {
15            threshold = (null != altThreshold)
16                    ? Integer.parseInt(altThreshold)
17                    : ALTERNATIVE_HASHING_THRESHOLD_DEFAULT;//默認爲Integer.MAX_VALUE
18
19            // disable alternative hashing if -1
20            if (threshold == -1) {
21                threshold = Integer.MAX_VALUE;
22            }
23
24            if (threshold < 0) {
25                throw new IllegalArgumentException("value must be positive integer.");
26            }
27        } catch(IllegalArgumentException failed) {
28            throw new Error("Illegal value for 'jdk.map.althashing.threshold'", failed);
29        }
30
31        ALTERNATIVE_HASHING_THRESHOLD = threshold;//指定可選的哈希閾值,在initHashSeedAsNeeded做爲是否初始化哈希種子的斷定條件
32    }
33}
34
35//根據容量決定是否須要初始化哈希種子
36final boolean initHashSeedAsNeeded(int capacity) {
37    boolean currentAltHashing = hashSeed != 0;//哈希種子默認爲0
38    boolean useAltHashing = sun.misc.VM.isBooted() &&
39            (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);//若是容量大於可選的哈希閾值,則須要初始化哈希種子
40    boolean switching = currentAltHashing ^ useAltHashing;
41    if (switching) {
42        hashSeed = useAltHashing
43            ? sun.misc.Hashing.randomHashSeed(this)//生成一個隨機的哈希種子
44            : 0;
45    }
46    return switching;
47}
複製代碼

擴容

一、源碼:

 1//按照指定容量進行數組擴容
2void resize(int newCapacity) {
3    Entry[] oldTable = table;
4    int oldCapacity = oldTable.length;
5    if (oldCapacity == MAXIMUM_CAPACITY) {//原有容量達到最大值,則再也不擴容
6        threshold = Integer.MAX_VALUE;
7        return;
8    }
9
10    Entry[] newTable = new Entry[newCapacity];
11    transfer(newTable, initHashSeedAsNeeded(newCapacity));
12    table = newTable;
13    threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);//按照擴容後容量從新計算閾值
14}
15
16//將元素從新分配到新數組中
17void transfer(Entry[] newTable, boolean rehash) {
18    int newCapacity = newTable.length;
19    for (Entry<K,V> e : table) {//遍歷原數組
20        while(null != e) {
21            Entry<K,V> next = e.next;
22            if (rehash) {//擴容後數組須要從新計算哈希
23                e.hash = null == e.key ? 0 : hash(e.key);
24            }
25            int i = indexFor(e.hash, newCapacity);//計算新數組中的位置
26            e.next = newTable[i];//採用頭插入法,添加到新數組中
27            newTable[i] = e;
28            e = next;
29        }
30    }
31}
複製代碼

二、問題:

     上述擴容代碼,在併發狀況下執行,就會出現常說的鏈表成環的問題,下面經過示例來分析:
2.一、初始狀態:
併發

圖注:初始狀態
圖注:初始狀態

     線程1插入18,線程2插入26。此時線程1發現size爲6,進行擴容。線程2發現size爲6,也進行擴容。
2.二、 線程1執行:
     線程1首先獲取到CPU執行權,執行transfer()中代碼:

 1for (Entry<K,V> e : table) {
2    while(null != e) {
3        Entry<K,V> next = e.next;//線程1執行到此行代碼,e爲10,next爲2。此時CPU調度線程2執行。
4        if (rehash) {
5            e.hash = null == e.key ? 0 : hash(e.key);
6        }
7        int i = indexFor(e.hash, newCapacity);
8        e.next = newTable[i];
9        newTable[i] = e;
10        e = next;
11    }
12}
複製代碼

2.三、 線程2執行:
     線程2此時獲取到CPU執行權,執行transfer()中代碼:app

 1for (Entry<K,V> e : table) {
2    while(null != e) {
3        Entry<K,V> next = e.next;
4        if (rehash) {
5            e.hash = null == e.key ? 0 : hash(e.key);
6        }
7        int i = indexFor(e.hash, newCapacity);
8        e.next = newTable[i];
9        newTable[i] = e;
10        e = next;
11    }
12}
複製代碼

     第一次遍歷:e爲10,next爲2,rehash爲false,i爲2,newTable[2]爲null,10.next爲null,newTable[2]爲10,e爲2。
     第二次遍歷:e爲2,next爲null,rehash爲false,i爲2,newTable[2]爲10,2.next爲10,newTable[2]爲2,e爲null。
     第三次遍歷:e爲null,退出循環。
     注意,此時原table中元素2的next指向了10。
dom

圖注:線程2執行擴容後結果
圖注:線程2執行擴容後結果

2.四、 線程1執行:

 1for (Entry<K,V> e : table) {
2    while(null != e) {
3        Entry<K,V> next = e.next;//線程1執行到此行代碼,e爲10,next爲2。CPU調度線程1繼續執行。
4        if (rehash) {
5            e.hash = null == e.key ? 0 : hash(e.key);
6        }
7        int i = indexFor(e.hash, newCapacity);
8        e.next = newTable[i];
9        newTable[i] = e;
10        e = next;
11    }
12}
複製代碼

     當前:e爲10,next爲2,rehash爲false,i爲2,newTable[2]爲null,修改:10.next爲null,newTable[2]爲10,e爲2。
     第二次遍歷:當前:e爲2,next爲10【線程2執行後的結果】,rehash爲false,i爲2,newTable[2]爲10,修改:2.next爲10,newTable[2]爲2,e爲10。
     第三次遍歷:當前:e爲10,next爲null,rehash爲false,i爲2,newTable[2]爲2,修改:10.next爲2,newTable[2]爲10,e爲null,退出循環。
此時,鏈表成環,若是進行查找,會陷入死循環!!!
函數

圖注:線程1執行擴容後結果
圖注:線程1執行擴容後結果

三、說明:

     由上例可知,HashMap在jdk1.7中採用頭插入法,在擴容時會改變鏈表中元素本來的順序,以致於在併發場景下致使鏈表成環的問題。而在jdk1.8中採用尾插入法,在擴容時會保持鏈表元素本來的順序,就不會出現鏈表成環的問題了。學習

總結

     經過上述的分析,在這裏總結下HashMap在1.7和1.8之間的變化:

  • 1.7採用數組+單鏈表,1.8在單鏈表超過必定長度後改爲紅黑樹存儲
  • 1.7擴容時須要從新計算哈希值和索引位置,1.8並不從新計算哈希值,巧妙地採用和擴容後容量進行&操做來計算新的索引位置。
  • 1.7插入元素到單鏈表中採用頭插入法,1.8採用的是尾插入法。

     經過對HashMap在jdk1.7和1.8中源碼的學習,深深地體會到一個道理:一切設計都有着它背後的緣由。做爲學習者,咱們須要不斷的問本身,爲何這麼設計,這麼設計有什麼好處。本着這樣的學習態度,我想不久的未來,你就會變成他。
     文章的最後,感謝你們的支持,歡迎掃描下方二維碼,進行關注。若有任何疑問,歡迎你們留言。


     還沒結束,哈哈。斗膽給你們分享一個足球故事:
     主人公是現役足球運動員姆巴佩,小時候家裏貼滿了C羅的海報。經過本身的不懈努力,最終成爲了一名職業運動員,而且成功和偶像成爲了對手。

圖注:小時候收藏的海報
圖注:小時候收藏的海報

圖注:長大後和偶像同場
圖注:長大後和偶像同場
相關文章
相關標籤/搜索