JDK1.7版本中的HashMap

以下講解基於JDK1.7



16e2d5411abebb9936a0a8ca46147371.png


HashMap底層是一個數組,哈希值相同的元素放在數組中的相同的位置,多個相同哈希值的元素形成一個鏈表。也就是說,元素的組織形式是單向鏈表。

下面從put、get、remove這三個方法分析一下源代碼,看看HashMap增刪查改是怎麼做的。

c1457a37a1263785263c4f295139c194.png

52c70c6639570c637115c51fce0b9edc.png

31ae5bd82dd27e186909af6f3e7f0824.png

構造HashMap對象的時候做了初始化,指定默認的初始容量(數組長度)和增長因子

接下來,從put開始分析


4266c06f740f92a3670abe063b774944.png

422ebfef9499513522b028f870209cf4.png

8549e057a56656e2bab37e2f90b13c13.png

從上面三段代碼可以看出添加一個元素的基本流程:

(1)HashMap的key值允許爲null,而且key爲null的元素放在數組中下標爲0的位置

(2)根據待插入元素的key值計算出一個哈希值,然後根據這個哈希值和數組的長度計算該元素將要放置的位置(PS:下標)。如果這個位置爲空,那麼直接插入。否則,遍歷該位置上的鏈表,依次比較他們的key值是否相同,如果相同,則將用新值替換舊值,然後返回就值。

(3)正常插入,將待插入的元素放置在鏈表的頭部,然後將其指向原先的鏈表頭部(即:原先放置在待插入位置的元素)。也就是說,新插入的元素是放在頭部的,我覺得這樣做的好處可能是根據LRU的原則減少遍歷的次數。

4aaa1f8dfd8c2aa5524e77b383f1d413.png

(4)有一種特殊情況是,擴容。

e353474a8dabc72e43738c338e3cda84.png

c22e2dfcf011e6bd26b17492f1fd5cae.png

9a6f0038999803732fd6a601ba1c3dc2.png

擴容就是,將原數組中的每個位置的元素都遷移到新數組中,在遷移到新數組的過程中同樣先計算哈希值,然後得出將要放置在新數組中那個位置上。鏈表的遷移過程相當於將原先的鏈表倒置,先將頭部的元素遷移過去,然後將下一個元素遷移過去,令next元素的next指向新數組位置上的元素,最終呈現出來的效果就是鏈表倒置。


接下來看get操作

a3ba157a678f086b90172ede47e6ee72.png

7f1b13557cabd806e2451490868996a9.png

get操作比較簡單:

(1)根據key算哈希值,進而得出元素可能的位置,然後遍歷該位置上的鏈表,比較key值是否相同,相同則返回,否則返回null


最後是remove


97e8dbac5c28d9a9fe3250c759858b34.png58c351170490d06642ff4e2e6b217824.png


刪除也相對比較簡單:

(1)刪除元素所在位置,遍歷鏈表,比較key值,找到待刪除元素以後,如果當前只有一個元素,直接刪除,此位置置位NULL,否則將前面元素的next指向後面元素。



HashMap在併發情況下存在的問題(併發就是沒法保證順序)

(1)插入的元素可能被覆蓋

75421e9d2f9efb86b0993eacc39674dc.png

假設有兩個線程都執行到這裏,線程1它的key=A,value=aaa,線程2它的key=B,value=bbb。

假設i=1,那麼線程1執行的時候table[1]=new Entry<>(1234, "A", "aaa", null);

等到線程2執行的時候table[1]=new Entry<>(1234, "B", "bbb", null);

於是乎,線程1插入的數據就丟失了(或者說是被覆蓋了)

(2)put的時候,鏈表可能形成環形數據結構,導致如果查找一個不存在的元素時死循環

那麼環狀是怎麼形成的呢?發生在擴容的時候。請看圖

fe7de554410a6b5077ada724afc4b907.png


假設有兩個元素A和B,它們的關係是A.next=B,B.next=null

大概就是下面這個樣子

3848a9c0cc5f7fe380da2d7a1605b807.png

假設有兩個線程,線程-1和線程-2,它們在執行插入的時候都發現需要擴容,於是乎都開始擴容。

當然,擴容是在它們自己的內存中進行的。假設線程-1完成對A元素的遷移後準備對B進行遷移並執行到Entry<K,V> next = e.next;時還沒執行時線程被掛起了。執行到線程-2先執行完擴容,於是擴容後的指向關係變成了這個樣子:B.next=A,A.next=null

c07b8da039ca7de381673f0b09bcba76.png

特別注意,看圖上畫的好像是元素直接放到數組的某個位置,但我們要知道,其它放的是元素的地址,也就是說元素本身的位置不變,修改的只是指針指向。儘管線程-2構造的新數組對線程-1而言是不可見的,但是不可否認,線程-2在擴容過程中已經將A和B的指向關係修改了,也就是說,此時,B是指向A的,這一點對線程-1而言是可見的。


接下來,線程-1醒來,繼續執行

while(null != e) {

    Entry<,> next = e.;
    (rehash) {
        e.= == e.? : hash(e.);
    }
    i = (e., newCapacity);
    e.= newTable[i];
    newTable[i] = e;
    e = next;
}

此時,對照代碼應該是這樣的

e5d8c692c771bfecb2bde936bee2164a.png

經過這一遍,現在在新數組中的指向關係變成:B-->A-->NULL

緊接着,因爲e已經是A了,所以null != e,於是再執行一遍

ecc603045ca1f804f44cce7d805a47a5.png


然後就變成這樣了

15c1dd235ec478750eab54e90b07bf39.png

接下來,麻煩來了。

查找C,經過計算C應該與A、B在數組的同一個位置,於是遍歷鏈表

ea09508843b2647c5dccd5874aaebc8c.png

於是,通過A找到B,通過B又找到A,通過A又找到B,通過B又找到A,如此反覆,永遠都不爲null,死循環



終於講明白了


最後,再提一點,就是hash方法,字符串和非字符串算哈希值的方法是不一樣的

cbb2f3f52dcfd68e2d21a2e1327ee8c9.png

參考:

http://blog.csdn.net/zhuqiuhui/article/details/51849692

https://www.cnblogs.com/binyue/p/3726403.html

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