HashMap整理

一、hashmap 數(shù)據(jù)結(jié)構(gòu)

HashMap 主要的操作一般包括:

  • V put(K key, V value)
  • V get(Object key)
  • V remove(Object key)
  • Boolean containsKey(Object key)

jdk7 之前的hashmap

HashMap內(nèi)部存儲數(shù)據(jù)的方式是通過內(nèi)部類Entry 來實現(xiàn)的混槐,其中Entry 類的結(jié)構(gòu)如下:

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

HashMap put操作

public V put(K key, V value) {
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
if (key == null)//key 為null,
return putForNullKey(value);
int hash = hash(key);//獲取key 的hash 值
int i = indexFor(hash, table.length);//將hash 值映射到數(shù)組中對應(yīng)的索引位置
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))) {//如果該元素在hashmap 中已存在赛糟,則替換原有的值
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(hash, key, value, i);//添加新的entry 元素到hashmap 中
return null;
}

hashmap內(nèi)部存儲的結(jié)構(gòu)尘执,其實就是一個entry 的數(shù)組礼搁,然后數(shù)組的每個位置(也稱之為bucket)存儲的是一個entry 的單鏈表瀑构,如下圖所示葫掉。


image.png
  • hash(key)相同的元素些举,存放在同一個entry 單鏈表中,
  • 不同的元素俭厚,存放在entry 數(shù)組中不同的bucket中
    當(dāng)進(jìn)行g(shù)et (key) 操作時户魏,首先會計算key對應(yīng)在數(shù)組的位置,然后根據(jù)key 的equal() 方法挪挤,遍歷entry 單鏈表 叼丑,找到對應(yīng)key 的值,反之 put(key,value)操作時類似扛门,不同之處在于鸠信,是將該元素插入到entry 鏈表的頭部。
    整個bucket 鏈表的形成過程可以分為如下幾步:
  1. 獲取key 的hashcode 值
  2. 為了減少hash 沖突论寨,需要對hashcode 值進(jìn)行rehash 操作 (java8中hashcode右移16位 與hashcode 做異或運(yùn)算星立,即高位與低位)
  3. 為了讓元素存放到數(shù)組中對應(yīng)的索引位置,需要對rehash 后的值與(數(shù)組長度-1)進(jìn)行位運(yùn)算葬凳。

    final int hash(Object k) {
    int h = hashSeed;
    if (0 != h && k instanceof String) {
    return sun.misc.Hashing.stringHash32((String) k);
    }
    h ^= k.hashCode();
    // This function ensures that hashCodes that differ only by
    // constant multiples at each bit position have a bounded
    // number of collisions (approximately 8 at default load factor).
    //讓hashcode 的每一位都參與到了運(yùn)算中來绰垂,減少了hash 沖突的發(fā)生
    h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);
    }
    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);
    }

hashmap 什么情況下出現(xiàn)單鏈表

在createEntry方法中,主要有兩步操作:

  • Entry<K,V> e = table[bucketIndex]獲取數(shù)組中原來索引index位置的entry對象火焰。
  • 創(chuàng)建一個新的entry 對象 放入到數(shù)組中索引index位置處劲装,table[bucketIndex] = new Entry<>(hash, key, value, e);,從Entry 的構(gòu)造函數(shù)就可以知道荐健,如果原來的索引index位置的數(shù)據(jù)對象e 不為null酱畅,則此時新entry對象next->e琳袄。

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);
}
/**
* Like addEntry except that this version is used when creating entries
* as part of Map construction or "pseudo-construction" (cloning,
* deserialization). This version needn't worry about resizing the table.
*
* Subclass overrides this to alter the behavior of HashMap(Map),
* clone, and readObject.
*/
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++;
}

hashmap 擴(kuò)容操作

hashmap 的一系列操作(get,put纺酸,remove)都會涉及到map中單鏈表的key遍歷查找操作窖逗,假使忽略修改操作,這也是一個非常消耗性能的issue餐蔬,例如碎紊,hashmap 的數(shù)組大小為16,存放20w的數(shù)據(jù)樊诺,在這種場景下仗考,每一個entry 鏈表的平均大小為20w/16,這種情況之下一個key 的遍歷查找次數(shù)和一個很可怕的數(shù)字词爬,在這種情況之下秃嗜,為了避免這種長度巨大的單鏈表的產(chǎn)生,hashmap有了一個自動擴(kuò)容的操作顿膨,即resize锅锨。下面兩個圖,分別展示了擴(kuò)容前后map 中數(shù)據(jù)分布情況恋沃。

image.png

hashmap 的默認(rèn)初始大小為16 DEFAULT_INITIAL_CAPACITY = 1 << 4必搞,負(fù)載因子為DEFAULT_LOAD_FACTOR = 0.75f

為什么hashmap 不是線程安全的?

因為囊咏,在hashmap 自動擴(kuò)容的過程中恕洲,get和put 操作時,有可能獲得是擴(kuò)容前的數(shù)據(jù)梅割,更糟糕的情況是霜第,并發(fā)情況下,多個線程同時進(jìn)行put 操作炮捧,此時可能會引起resize 操作庶诡,導(dǎo)致entry 鏈表中形成一個循環(huán)鏈表。

jdk8 hashmap

java8 中hashmap 有了很大的改進(jìn)咆课,java8 中hashmap 內(nèi)部還是通過數(shù)組的存儲方式,只不過扯俱,數(shù)組中存儲的單元由java7 中的Entry 換成了Node,但是類結(jié)構(gòu)和包含的信息是一樣的书蚪,與java7 最大的不同之處在于Node 可以擴(kuò)展成TreeNode,其中TreeNode 可以擴(kuò)展成紅黑樹的數(shù)據(jù)結(jié)構(gòu)迅栅,如下圖所示殊校。

static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;

java8 hashmap數(shù)據(jù)結(jié)構(gòu).png

什么情況下擴(kuò)展成紅黑樹呢?

public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}

/**
 * Implements Map.put and related methods
 *
 * @param hash hash for key
 * @param key the key
 * @param value the value to put
 * @param onlyIfAbsent if true, don't change existing value
 * @param evict if false, the table is in creation mode.
 * @return previous value, or null if none
 */
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
  //當(dāng)前hashmap 為空,觸發(fā)map 的自動擴(kuò)容resize 操作
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
 //i = (n - 1) & hash 就是java7 中的indexFor 的操作读存,判斷數(shù)組當(dāng)前位置是否已經(jīng)為空为流,如果為空呕屎,就實例化一個node,將該node放入數(shù)組中這個索引位置
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;
 //key 存在敬察,就替換掉原有的value,與java 7 類似
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
     //當(dāng)單鏈表的長度大于等于TREEIFY_THRESHOLD - 1時秀睛,鏈表就轉(zhuǎn)換成一棵紅黑樹了
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

為什么HashMap 數(shù)組大小是2的冪次方

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

假如數(shù)組的長度為17,參與位運(yùn)算值為length-1=16莲祸,對應(yīng)的二進(jìn)制為0000,....,0001,0000,此時任意一個數(shù)h與16 進(jìn)行位運(yùn)算蹂安,結(jié)果只有16,0兩種結(jié)果,這意味著數(shù)組只有兩個bucket锐帜,再有元素存放hashmap 時田盈,必然會發(fā)生hash 沖突,導(dǎo)致hashmap 變成了兩個單鏈表缴阎。
而假如數(shù)組長度為16允瞧,參與位運(yùn)算的值為15,對應(yīng)的二進(jìn)制為0000,....,0000,1111蛮拔,瓷式,此時任意一個數(shù)與15 位運(yùn)算后,結(jié)果必然在[0,15]之間语泽,這剛好對應(yīng)著數(shù)組的下標(biāo)索引贸典。
這就是為什么hashmap 中數(shù)組的大小必須為2 的冪次方。

為什么 hashmap 會出現(xiàn)死循環(huán)

疫苗:JAVA HASHMAP的死循環(huán)

參考

疫苗:JAVA HASHMAP的死循環(huán)
JDK7與JDK8中HashMap的實現(xiàn)
How does a HashMap work in JAVA
(譯)Java7/Java8中HashMap解析

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末踱卵,一起剝皮案震驚了整個濱河市廊驼,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌惋砂,老刑警劉巖妒挎,帶你破解...
    沈念sama閱讀 222,946評論 6 518
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異西饵,居然都是意外死亡酝掩,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,336評論 3 399
  • 文/潘曉璐 我一進(jìn)店門眷柔,熙熙樓的掌柜王于貴愁眉苦臉地迎上來期虾,“玉大人,你說我怎么就攤上這事驯嘱∠獍” “怎么了?”我有些...
    開封第一講書人閱讀 169,716評論 0 364
  • 文/不壞的土叔 我叫張陵鞠评,是天一觀的道長茂蚓。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么聋涨? 我笑而不...
    開封第一講書人閱讀 60,222評論 1 300
  • 正文 為了忘掉前任晾浴,我火速辦了婚禮,結(jié)果婚禮上牍白,老公的妹妹穿的比我還像新娘脊凰。我一直安慰自己,他們只是感情好淹朋,可當(dāng)我...
    茶點故事閱讀 69,223評論 6 398
  • 文/花漫 我一把揭開白布笙各。 她就那樣靜靜地躺著,像睡著了一般础芍。 火紅的嫁衣襯著肌膚如雪杈抢。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,807評論 1 314
  • 那天仑性,我揣著相機(jī)與錄音惶楼,去河邊找鬼。 笑死诊杆,一個胖子當(dāng)著我的面吹牛歼捐,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播晨汹,決...
    沈念sama閱讀 41,235評論 3 424
  • 文/蒼蘭香墨 我猛地睜開眼豹储,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了淘这?” 一聲冷哼從身側(cè)響起剥扣,我...
    開封第一講書人閱讀 40,189評論 0 277
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎铝穷,沒想到半個月后钠怯,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,712評論 1 320
  • 正文 獨居荒郊野嶺守林人離奇死亡曙聂,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,775評論 3 343
  • 正文 我和宋清朗相戀三年晦炊,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片宁脊。...
    茶點故事閱讀 40,926評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡断国,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出朦佩,到底是詐尸還是另有隱情并思,我是刑警寧澤,帶...
    沈念sama閱讀 36,580評論 5 351
  • 正文 年R本政府宣布语稠,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏仙畦。R本人自食惡果不足惜输涕,卻給世界環(huán)境...
    茶點故事閱讀 42,259評論 3 336
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望慨畸。 院中可真熱鬧莱坎,春花似錦、人聲如沸寸士。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,750評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽弱卡。三九已至乃正,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間婶博,已是汗流浹背瓮具。 一陣腳步聲響...
    開封第一講書人閱讀 33,867評論 1 274
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留凡人,地道東北人名党。 一個月前我還...
    沈念sama閱讀 49,368評論 3 379
  • 正文 我出身青樓,卻偏偏與公主長得像挠轴,于是被迫代替她去往敵國和親传睹。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,930評論 2 361

推薦閱讀更多精彩內(nèi)容

  • 一、基本數(shù)據(jù)類型 注釋 單行注釋:// 區(qū)域注釋:/* */ 文檔注釋:/** */ 數(shù)值 對于byte類型而言...
    龍貓小爺閱讀 4,268評論 0 16
  • 實際上委煤,HashSet 和 HashMap 之間有很多相似之處堂油,對于 HashSet 而言,系統(tǒng)采用 Hash 算...
    曹振華閱讀 2,515評論 1 37
  • 隨著現(xiàn)代科技的發(fā)展碧绞,信息化時代已經(jīng)到來府框。我們的生活漸漸被種類繁多、花樣百變的電子產(chǎn)品所占據(jù)讥邻。電視迫靖、電腦、手機(jī)等電子...
    魯?shù)堑婪?/span>閱讀 1,033評論 0 0
  • 為人母 將三年 好像已經(jīng)不習(xí)慣沒有閨女在身邊 感謝她爹 陪伴閨女 值此紀(jì)念416一年の聚
    佳丫頭閱讀 199評論 0 0
  • 此刻我在南方的雨聲中醒來 北方的雨和南方會有什么不同 我曾晚上徒步了十公里路去看海 在上坡和下坡之間仰望和低俯 請...
    迷惘之鄉(xiāng)閱讀 79評論 0 0