1. 程式人生 > >JDK1.7版本中的HashMap

JDK1.7版本中的HashMap

hashmap

以下講解基於JDK1.7



技術分享圖片


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

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

技術分享圖片

技術分享圖片

技術分享圖片

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

接下來,從put開始分析


技術分享圖片

技術分享圖片

技術分享圖片

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

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

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

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

技術分享圖片

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

技術分享圖片

技術分享圖片

技術分享圖片

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


接下來看get操作

技術分享圖片

技術分享圖片

get操作比較簡單:

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


最後是remove


技術分享圖片技術分享圖片


刪除也相對比較簡單:

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



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

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

技術分享圖片

假設有兩個線程都執行到這裏,線程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的時候,鏈表可能形成環形數據結構,導致如果查找一個不存在的元素時死循環

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

技術分享圖片


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

大概就是下面這個樣子

技術分享圖片

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

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

技術分享圖片

特別註意,看圖上畫的好像是元素直接放到數組的某個位置,但我們要知道,其它放的是元素的地址,也就是說元素本身的位置不變,修改的只是指針指向。盡管線程-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;
}

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

技術分享圖片

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

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

技術分享圖片


然後就變成這樣了

技術分享圖片

接下來,麻煩來了。

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

技術分享圖片

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



終於講明白了


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

技術分享圖片

參考:

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

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

本文出自 “不要亂摸” 博客,請務必保留此出處http://5880861.blog.51cto.com/5870861/1984059

JDK1.7版本中的HashMap