HashMap與HashTable區(qū)別以及原理解析

前言

需要知道的是幢泼,HashMap在Java8里發(fā)生了比較大的變化。在Java8中劲腿,table的桶(bucket)中旭绒,每個(gè)桶跟之前一樣,是維護(hù)一個(gè)鏈表焦人。但是如果鏈表大小超過閾值(TREEIFY_THRESHOLD=8)挥吵,鏈表就會(huì)被改造為紅黑樹結(jié)構(gòu)。
這是因?yàn)榛ㄍ郑诖罅縣ash碰撞下忽匈,Map就會(huì)被弱化為類似鏈表的結(jié)構(gòu)(全部存在一個(gè)bucket中),這樣會(huì)大大降低性能矿辽。在超過閾值以后丹允,改為紅黑樹的方式,可以提高查找性能袋倔。

本文最下方還介紹了 LinkedHashMap 和 TreeMap 的區(qū)別雕蔽。


Map整體結(jié)構(gòu)

Map通常被包含在集合框架里,但是Map并不繼承自 Collection 接口宾娜,最底層接口是Map批狐。
類的繼承結(jié)構(gòu)可以參考下圖:

Map整體結(jié)構(gòu)圖



HashMap和HashTable區(qū)別

  1. 最主要區(qū)別在于線程安全,HashMap不是線程安全的前塔,而HashTable是線程安全的嚣艇。
    hashtable內(nèi)部添加了synchronized關(guān)鍵字來確保線程同步。而HashMap沒有华弓,因此性能上來說食零,HashMap會(huì)相對(duì)高一些。
    我們平時(shí)使用一般情況下是用HashMap寂屏,而如果要在多線程情況下使用HashMap贰谣,可以使用Collections.synchronizedMap()來獲取一個(gè)線程安全的集合娜搂。

Collections.synchronizedMap() 的實(shí)現(xiàn)原理是,Collections內(nèi)部定義了一個(gè)SynchronizedMap的內(nèi)部類冈爹,這個(gè)類實(shí)現(xiàn)了Map接口涌攻,使用synchronized關(guān)鍵字在復(fù)寫的方法中實(shí)現(xiàn)線程安全,然后操作傳入的實(shí)例對(duì)象频伤。

//以下是Collections.SynchronizedMap 內(nèi)部的put方法實(shí)現(xiàn)
public V put(K key, V value) {
    synchronized (mutex) {return m.put(key, value);}
}


  1. HashMap可以使用null作為key,而HashTable不允許這樣做芝此。
    雖然HashMap支持null作為key憋肖,但一般不建議這樣做。因?yàn)檫@樣做比較坑╮(╯▽╰)╭婚苹。另外HastTable已經(jīng)不建議在代碼中使用岸更,如果需要線程安全的場(chǎng)景,建議使用ConcurrentHashMap
    順便說一下膊升,如果HashMap以null作為key的話怎炊,Entry節(jié)點(diǎn)對(duì)象總是存在table數(shù)組的第一個(gè)節(jié)點(diǎn)上

  2. HashMap是對(duì)Map接口的實(shí)現(xiàn),HashTable在實(shí)現(xiàn)了Map接口的同時(shí)廓译,繼承了Dictionary抽象類评肆。
    這倆都實(shí)現(xiàn)了 Map Coneable 和 Serializable 接口,但是沒有實(shí)現(xiàn)Collection接口非区。

  3. 構(gòu)造器有兩個(gè)可選參數(shù):int initialCapacity, float loadFactor瓜挽。分別表示初始容量 和,加載因子征绸。
    兩個(gè)默認(rèn)的加載因子都是0.75久橙。如果容量達(dá)到荷載因子*size的話,就要進(jìn)行擴(kuò)容(resize)管怠,擴(kuò)容的同時(shí)要重新計(jì)算每個(gè)元素在新數(shù)組中的位置淆衷,因此性能開銷較大。
    HashMap的初始容量是16渤弛,擴(kuò)容時(shí)按2的指數(shù)冪擴(kuò)容祝拯。
    HashTable的初始容量是11,擴(kuò)容時(shí)按2的指數(shù)冪+1擴(kuò)容
    而關(guān)于加載因子為什么選用0.75

Hash表為解決沖突暮芭,可以使用開放地址法和鏈地址法來解決鹿驼,Java的HashTable采用了鏈地址法,即數(shù)組+鏈表辕宏。荷載因子就是一個(gè)特別重要的參數(shù)畜晰,應(yīng)該嚴(yán)格限制在0.7~0.8以下。超過0.8查表時(shí)CPU緩存不命中概率將按照指數(shù)級(jí)上升瑞筐,嚴(yán)重降低性能凄鼻。hash表的平均查找長(zhǎng)度,是關(guān)于荷載因子a的函數(shù)


  1. 兩者計(jì)算hash的方法不同
    都是根據(jù)key進(jìn)行hash運(yùn)算后確定存儲(chǔ)地址index。
    HashTable計(jì)算hash是用key的hash對(duì)數(shù)組長(zhǎng)度進(jìn)行取模運(yùn)算
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;

而hashMap為了減少Hash碰撞概率块蚌,進(jìn)行了二次hash
以下是源碼實(shí)現(xiàn)闰非。注意jdk1.7和jdk1.8的實(shí)現(xiàn)方法不一樣

//JDK1.7 方法一:
final int hash(Object k) {
        int h = 0;
        if (useAltHashing) {
            if (k instanceof String) {
                return sun.misc.Hashing.stringHash32((String) k);
            }
            h = hashSeed;
        }

        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).
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
}

//JDK1.8 方法一:
static final int hash(Object key) {   //jdk1.8
     int h;
     // h = key.hashCode() 為第一步 取hashCode值
     // h ^ (h >>> 16)  為第二步 高位參與運(yùn)算
     return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
//方法二:
static int indexFor(int h, int length) {  //jdk1.7的源碼,jdk1.8沒有這個(gè)方法峭范,但是實(shí)現(xiàn)原理一樣的
     return h & (length-1);  //第三步 取模運(yùn)算
}

這里要注意财松,為什么這里需要將高位數(shù)據(jù)移位到低位進(jìn)行異或運(yùn)算呢?
這是因?yàn)橛行?shù)據(jù)計(jì)算出的哈希值差異主要在高位纱控,而 HashMap 里的哈希尋址是忽略容量以上的高位的辆毡,那么這種處理就可以有效避免類似情況下的哈希碰撞。

為了分布均勻甜害,我們首先想到的就是把hash值對(duì)數(shù)組長(zhǎng)度取模運(yùn)算舶掖,這樣元素的分布相對(duì)均勻。但是取模運(yùn)算對(duì)于性能消耗還是比較大的尔店。
在HashMap中是這樣做的眨攘,調(diào)用方法二來計(jì)算index。
這個(gè)方法非常巧妙嚣州。它通過h & (table.length -1)得到該對(duì)象的保存位鲫售,而HashMap低層數(shù)組的長(zhǎng)度總是2的n次方,這是HashMap在速度上的優(yōu)化避诽。而當(dāng)length總是等2的n次方時(shí)龟虎,h & (table.length -1) 運(yùn)算等價(jià)于對(duì)length取模,也就是h%table.length沙庐,但是&操作更有效率鲤妥。

  1. HashMap和HashTable都是 使用鏈地址法解決Hash碰撞,使用 數(shù)組+鏈表 的方式存儲(chǔ)拱雏。(JDK1.8中鏈表改為了紅黑樹)
    如果hash碰撞棉安,則會(huì)用equals()比較key到底是否相同。相同則覆蓋铸抑,如果不相同贡耽,則會(huì)放到相同的bukectIndex下面,用鏈表存儲(chǔ)下一個(gè)元素的位置鹊汛。
    Entry是HashMap的一個(gè)靜態(tài)內(nèi)部類蒲赂,實(shí)現(xiàn)一個(gè)鏈表結(jié)構(gòu),保存了之前的Enrty對(duì)象為next元素刁憋。
    以下為JDK1.7的Map部分源碼滥嘴。
static class Entry<K,V> implements Map.Entry<K,V>{ 
       final K key;
        V value;
        Entry<K,V> next;
        int hash;

        /**
         * Creates new entry.
         */
        Entry(int h, K k, V v, Entry<K,V> n) {
            value = v;
            next = n;
            key = k;
            hash = h;
        }
}

//put源碼 JDK1.7
public V put(K key, V value) {
        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;
    }

//校驗(yàn)并擴(kuò)容
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);
    }

//創(chuàng)建Entry節(jié)點(diǎn),相同bucketIndex創(chuàng)建為鏈表節(jié)點(diǎn)
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和HashTable的實(shí)現(xiàn)原理

  • resize()方法
    由第6點(diǎn)可以看到至耻,在addEntry()方法內(nèi)若皱,每次執(zhí)行createEntry()之前镊叁,都需要校驗(yàn)一遍是否需要擴(kuò)容。
    (size >= threshold) && (null != table[bucketIndex])
    如果size達(dá)到了臨界值走触,并且當(dāng)前要添加的節(jié)點(diǎn)不為null晦譬。就要進(jìn)行擴(kuò)容。
    具體過程為:
    先創(chuàng)建一個(gè)容量為table.length*2(也就是2次方翻倍)的新table互广,修改臨界值敛腌。然后把HashMap里的所有元素根據(jù)新的length重新計(jì)算index放入新的table中。
    這里要注意兜辞,是用 每個(gè)元素的hash重新計(jì)算Index迎瞧,而不是簡(jiǎn)單的把原table的index放到新table中。(因?yàn)橛?jì)算index時(shí)逸吵,是根據(jù)length長(zhǎng)度均勻分布的)所以resize()是比較消耗性能的操作。如果在創(chuàng)建map時(shí)能大概預(yù)見到大小缝裁,那么設(shè)置一個(gè)相對(duì)大點(diǎn)的初始容量是一個(gè)不錯(cuò)的選擇扫皱。
    resize()源碼就不放了。JDK1.8有新實(shí)現(xiàn)

  • hash() 和 indexFor()
    hash()是方法是為了對(duì)key.hashCode()進(jìn)行二次散列捷绑,以獲得更好的散列值韩脑,減少碰撞概率。
    indexFor()是根據(jù)散列值計(jì)算出存儲(chǔ)在table中的下標(biāo)粹污,與table.length取模段多。均勻分布。



LinkedHashMap 和 TreeMap

這兩種Map都可以保證某種順序壮吩,但是實(shí)現(xiàn)原理是不同的进苍。

  • LinkedHashMap

提供的是遍歷順序符合插入順序,它的實(shí)現(xiàn)是通過為Entry(鍵值對(duì))維護(hù)一個(gè)雙向鏈表鸭叙。
部分源碼如下:

/**
 * HashMap.Node subclass for normal LinkedHashMap entries.
 */
static class Entry<K,V> extends HashMap.Node<K,V> {
    Entry<K,V> before, after;
    Entry(int hash, K key, V value, Node<K,V> next) {
        super(hash, key, value, next);
    }
}


  • TreeMap
    它的整體順序是由鍵來確定的觉啊,通過 Comparator 或 Comparable(自然順序)來決定。



最后說一下HashSet

內(nèi)部實(shí)現(xiàn)是一個(gè)HashMap沈贝,只不過value是一個(gè)固定對(duì)象杠人。
HashSet部分源碼如下

private static final Object PRESENT = new Object();
private transient HashMap<E,Object> map;
public boolean add(E e) {
    return map.put(e, PRESENT)==null;
}

參考資料:
HashMap原理解析(較全面)
HashMap與HashTable實(shí)現(xiàn)原理淺析
Java8系列之重新認(rèn)識(shí)HashMap(深度解析)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市宋下,隨后出現(xiàn)的幾起案子嗡善,更是在濱河造成了極大的恐慌,老刑警劉巖学歧,帶你破解...
    沈念sama閱讀 222,378評(píng)論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件罩引,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡撩满,警方通過查閱死者的電腦和手機(jī)蜒程,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,970評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門绅你,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人昭躺,你說我怎么就攤上這事忌锯。” “怎么了领炫?”我有些...
    開封第一講書人閱讀 168,983評(píng)論 0 362
  • 文/不壞的土叔 我叫張陵偶垮,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我帝洪,道長(zhǎng)似舵,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,938評(píng)論 1 299
  • 正文 為了忘掉前任葱峡,我火速辦了婚禮砚哗,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘砰奕。我一直安慰自己蛛芥,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,955評(píng)論 6 398
  • 文/花漫 我一把揭開白布军援。 她就那樣靜靜地躺著仅淑,像睡著了一般。 火紅的嫁衣襯著肌膚如雪胸哥。 梳的紋絲不亂的頭發(fā)上涯竟,一...
    開封第一講書人閱讀 52,549評(píng)論 1 312
  • 那天,我揣著相機(jī)與錄音空厌,去河邊找鬼庐船。 笑死,一個(gè)胖子當(dāng)著我的面吹牛蝇庭,可吹牛的內(nèi)容都是我干的醉鳖。 我是一名探鬼主播,決...
    沈念sama閱讀 41,063評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼哮内,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼盗棵!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起北发,我...
    開封第一講書人閱讀 39,991評(píng)論 0 277
  • 序言:老撾萬榮一對(duì)情侶失蹤纹因,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后琳拨,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體瞭恰,經(jīng)...
    沈念sama閱讀 46,522評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,604評(píng)論 3 342
  • 正文 我和宋清朗相戀三年狱庇,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了惊畏。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片恶耽。...
    茶點(diǎn)故事閱讀 40,742評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖颜启,靈堂內(nèi)的尸體忽然破棺而出偷俭,到底是詐尸還是另有隱情,我是刑警寧澤缰盏,帶...
    沈念sama閱讀 36,413評(píng)論 5 351
  • 正文 年R本政府宣布涌萤,位于F島的核電站,受9級(jí)特大地震影響口猜,放射性物質(zhì)發(fā)生泄漏负溪。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,094評(píng)論 3 335
  • 文/蒙蒙 一济炎、第九天 我趴在偏房一處隱蔽的房頂上張望川抡。 院中可真熱鬧,春花似錦须尚、人聲如沸猖腕。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,572評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至放坏,卻和暖如春咙咽,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背淤年。 一陣腳步聲響...
    開封第一講書人閱讀 33,671評(píng)論 1 274
  • 我被黑心中介騙來泰國(guó)打工钧敞, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人麸粮。 一個(gè)月前我還...
    沈念sama閱讀 49,159評(píng)論 3 378
  • 正文 我出身青樓溉苛,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親弄诲。 傳聞我的和親對(duì)象是個(gè)殘疾皇子愚战,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,747評(píng)論 2 361

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