HashMap解讀


1.什么是HashMap

基于哈希表的 Map 接口的實現(xiàn)。此實現(xiàn)提供所有可選的映射操作时鸵,并允許使用 null 值和 null 鍵筋搏。(除了非同步和允許使用 null 之外包蓝,HashMap 類與 Hashtable 大致相同。)此類不保證映射的順序绽淘,特別是它不保證該順序恒久不變涵防。 此實現(xiàn)假定哈希函數(shù)將元素適當(dāng)?shù)胤植荚诟魍爸g,可為基本操作(get 和 put)提供穩(wěn)定的性能。迭代 collection 視圖所需的時間與 HashMap 實例的“容量”(桶的數(shù)量)及其大凶吵亍(鍵-值映射關(guān)系數(shù))成比例偏瓤。所以,如果迭代性能很重要椰憋,則不要將初始容量設(shè)置得太高(或?qū)⒓虞d因子設(shè)置得太低)厅克。

2.HashMap和HashTable的區(qū)別

  1. HashTable的方法是同步的,在方法的前面都有synchronized來同步橙依,HashMap未經(jīng)同步证舟,所以在多線程場合要手動同步。
  2. HashTable不允許null值(key和value都不可以) ,HashMap允許null值(key和value都可以)窗骑。
  3. HashTable有一個contains(Object value)功能和containsValue(Object value)功能一樣女责。
  4. HashTable使用Enumeration進行遍歷,HashMap使用Iterator進行遍歷创译。
  5. HashTable中hash數(shù)組默認大小是11抵知,增加的方式是 old*2+1。HashMap中hash數(shù)組的默認大小是16软族,而且一定是2的指數(shù)刷喜。
  6. 哈希值的使用不同,HashTable直接使用對象的hashCode立砸,代碼是這樣的:

int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;

而HashMap重新計算hash值掖疮,而且用與代替求模:
```java

int hash = hash(k);
int i = indexFor(hash, table.length);
static int hash(Object x) {
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
static int indexFor(int h, int length) {
return h & (length-1);
}

3. HashMap與HashSet的關(guān)系

  1. HashSet底層是采用HashMap實現(xiàn)的:

public HashSet() {
map = new HashMap<E,Object>();
}

2. 調(diào)用HashSet的add方法時,實際上是向HashMap中增加了一行(key-value對)仰禽,該行的key就是向HashSet增加的那個對象氮墨,該行的value就是一個Object類型的常量。

4. HashMap 和 ConcurrentHashMap 的關(guān)系

關(guān)于這部分內(nèi)容建議自己去翻翻源碼吐葵,ConcurrentHashMap
也是一種線程安全的集合類规揪,他和HashTable也是有區(qū)別的,主要區(qū)別就是加鎖的粒度以及如何加鎖温峭,ConcurrentHashMap 的加鎖粒度要比HashTable更細一點猛铅。將數(shù)據(jù)分成一段一段的存儲,然后給每一段數(shù)據(jù)配一把鎖凤藏,當(dāng)一個線程占用鎖訪問其中一個段數(shù)據(jù)的時候奸忽,其他段的數(shù)據(jù)也能被其他線程訪問。

5. HashMap實現(xiàn)原理分析

HashMap的數(shù)據(jù)結(jié)構(gòu) 數(shù)據(jù)結(jié)構(gòu)中有數(shù)組鏈表來實現(xiàn)對數(shù)據(jù)的存儲揖庄,但這兩者基本上是兩個極端栗菜。

數(shù)組:數(shù)組必須事先定義固定的長度(元素個數(shù)),不能適應(yīng)數(shù)據(jù)動態(tài)地增減的情況蹄梢。當(dāng)數(shù)據(jù)增加時疙筹,可能超出原先定義的元素個數(shù);當(dāng)數(shù)據(jù)減少時,造成內(nèi)存浪費而咆。

數(shù)組是靜態(tài)分配內(nèi)存霍比,并且在內(nèi)存中連續(xù)。
數(shù)組利用下標(biāo)定位暴备,時間復(fù)雜度為O(1)
數(shù)組插入或刪除元素的時間復(fù)雜度O(n)
數(shù)組的特點是:尋址容易悠瞬,插入和刪除困難;

鏈表:鏈表存儲區(qū)間離散涯捻,占用內(nèi)存比較寬松浅妆。

鏈表是動態(tài)分配內(nèi)存,并不連續(xù)汰瘫。
鏈表定位元素時間復(fù)雜度O(n)
鏈表插入或刪除元素的時間復(fù)雜度O(1)
鏈表的特點是:尋址困難狂打,插入和刪除容易。

哈希表

那么我們能不能綜合兩者的特性混弥,做出一種尋址容易趴乡,插入刪除也容易的數(shù)據(jù)結(jié)構(gòu)?答案是肯定的蝗拿,這就是我們要提起的哈希表晾捏。哈希表((Hash table)既滿足了數(shù)據(jù)的查找方便,同時不占用太多的內(nèi)容空間哀托,使用也十分方便惦辛。

哈希表有多種不同的實現(xiàn)方法,我接下來解釋的是最常用的一種方法—— 拉鏈法仓手,我們可以理解為“鏈表的數(shù)組” 胖齐,如圖:


code
code

從上圖我們可以發(fā)現(xiàn)哈希表是由數(shù)組+鏈表組成的,一個長度為16的數(shù)組中嗽冒,每個元素存儲的是一個鏈表的頭結(jié)點呀伙。那么這些元素是按照什么樣的規(guī)則存儲到數(shù)組中呢。一般情況是通過hash(key)%len獲得添坊,也就是元素的key的哈希值對數(shù)組長度取模得到剿另。比如上述哈希表中,12%16=12,28%16=12,108%16=12,140%16=12贬蛙。所以12雨女、28、108以及140都存儲在數(shù)組下標(biāo)為12的位置阳准。   HashMap其實也是一個線性的數(shù)組實現(xiàn)的,所以可以理解為其存儲數(shù)據(jù)的容器就是一個線性數(shù)組氛堕。這可能讓我們很不解,一個線性的數(shù)組怎么實現(xiàn)按鍵值對來存取數(shù)據(jù)呢野蝇?這里HashMap有做一些處理岔擂。   首先HashMap里面實現(xiàn)一個靜態(tài)內(nèi)部類Entry位喂,其重要的屬性有 key , value, next浪耘,從屬性key,value我們就能很明顯的看出來Entry就是HashMap鍵值對實現(xiàn)的一個基礎(chǔ)bean乱灵,我們上面說到HashMap的基礎(chǔ)就是一個線性數(shù)組,這個數(shù)組就是Entry[]七冲,Map里面的內(nèi)容都保存在Entry[]里面痛倚。

6. HashMap的存取實現(xiàn)

既然是線性數(shù)組,為什么能隨機存壤教伞蝉稳?這里HashMap用了一個小算法,大致是這樣實現(xiàn):

// 存儲時:
int hash = key.hashCode(); // 這個hashCode方法這里不詳述,只要理解每個key的hash是一個固定的int值
int index = hash % Entry[].length;
Entry[index] = value;

// 取值時:
int hash = key.hashCode();
int index = hash % Entry[].length;
return Entry[index];
  • put

疑問:如果兩個key通過hash%Entry[].length得到的index相同掘鄙,會不會有覆蓋的危險耘戚?

這里HashMap里面用到鏈?zhǔn)綌?shù)據(jù)結(jié)構(gòu)的一個概念。上面我們提到過Entry類里面有一個next屬性操漠,作用是指向下一個Entry收津。

打個比方, 第一個鍵值對A進來浊伙,通過計算其key的hash得到的index=0撞秋,記做:Entry[0] = A。一會后又進來一個鍵值對B嚣鄙,通過計算其index也等于0吻贿,現(xiàn)在怎么辦?HashMap會這樣做:B.next = A,Entry[0] = B,如果又進來C,index也等于0,那么C.next = B,Entry[0] = C哑子;這樣我們發(fā)現(xiàn)index=0的地方其實存取了A,B,C三個鍵值對,他們通過next這個屬性鏈接在一起舅列。所以疑問不用擔(dān)心。也就是說數(shù)組中存儲的是最后插入的元素卧蜓。到這里為止帐要,HashMap的大致實現(xiàn),我們應(yīng)該已經(jīng)清楚了烦却。

public V put(K key, V value) {
       if (key == null)
            return putForNullKey(value); //null總是放在數(shù)組的第一個鏈表中
        int hash = hash(key.hashCode());
        int i = indexFor(hash, table.length);
        //遍歷鏈表
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            //如果key在鏈表中已存在宠叼,則替換為新value
            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;
}

void addEntry(int hash, K key, V value, int bucketIndex) {
    Entry<K,V> e = table[bucketIndex];
    table[bucketIndex] = new Entry<K,V>(hash, key, value, e); //參數(shù)e, 是Entry.next
    //如果size超過threshold,則擴充table大小其爵。再散列
    if (size++ >= threshold)
            resize(2 * table.length);
}

當(dāng)然HashMap里面也包含一些優(yōu)化方面的實現(xiàn)冒冬,這里也說一下。比如:Entry[]的長度一定后摩渺,隨著map里面數(shù)據(jù)的越來越長简烤,這樣同一個index的鏈就會很長,會不會影響性能摇幻?HashMap里面設(shè)置一個因子横侦,隨著map的size越來越大挥萌,Entry[]會以一定的規(guī)則加長長度。

  • get
public V get(Object key) {
        if (key == null)
            return getForNullKey();
        int hash = hash(key.hashCode());
        //先定位到數(shù)組元素枉侧,再遍歷該元素處的鏈表
        for (Entry<K,V> e = table[indexFor(hash, table.length)];
             e != null;
             e = e.next) {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
                return e.value;
        }
        return null;
}
  • null key的存取
    null key總是存放在Entry[]數(shù)組的第一個元素引瀑。
private V putForNullKey(V value) {
    for (Entry<K,V> e = table[0]; e != null; e = e.next) {
        if (e.key == null) {
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }
    modCount++;
    addEntry(0, null, value, 0);
    return null;
}


private V getForNullKey() {
    for (Entry<K,V> e = table[0]; e != null; e = e.next) {
        if (e.key == null)
            return e.value;
    }
    return null;
}
  • 確定數(shù)組index:hashcode % table.length取模
    HashMap存取時,都需要計算當(dāng)前key應(yīng)該對應(yīng)Entry[]數(shù)組哪個元素榨馁,即計算數(shù)組下標(biāo)憨栽;算法如下:
 /**
 * Returns index for hash code h.
 */
static int indexFor(int h, int length) {
    return h & (length-1);
}

按位取并,作用上相當(dāng)于取模mod或者取余%翼虫。 這意味著數(shù)組下標(biāo)相同屑柔,并不表示hashCode相同。

  • table初始大小
public HashMap(int initialCapacity, float loadFactor) {
    // Find a power of 2 >= initialCapacity
    int capacity = 1;
    while (capacity < initialCapacity)
        capacity <<= 1;
    this.loadFactor = loadFactor;
    threshold = (int)(capacity * loadFactor);
    table = new Entry[capacity];
    init();
}

7. 解決hash沖突的辦法

開放定址法(線性探測再散列珍剑,二次探測再散列掸宛,偽隨機探測再散列) 再哈希法 鏈地址法 建立一個公共溢出區(qū) Java中hashmap的解決辦法就是采用的鏈地址法。

8. 再散列rehash過程

當(dāng)哈希表的容量超過默認容量時招拙,必須調(diào)整table的大小唧瘾。當(dāng)容量已經(jīng)達到最大可能值時,那么該方法就將容量調(diào)整到Integer.MAX_VALUE返回迫像,這時劈愚,需要創(chuàng)建一張新表,將原表的映射到新表中闻妓。

/**
 * Rehashes the contents of this map into a new array with a
 * larger capacity.  This method is called automatically when the
 * number of keys in this map reaches its threshold.
 *
 * If current capacity is MAXIMUM_CAPACITY, this method does not
 * resize the map, but sets threshold to Integer.MAX_VALUE.
 * This has the effect of preventing future calls.
 *
 * @param newCapacity the new capacity, MUST be a power of two;
 *        must be greater than current capacity unless current
 *        capacity is MAXIMUM_CAPACITY (in which case value
 *        is irrelevant).
 */
void resize(int newCapacity) {
    Entry[] oldTable = table;
    int oldCapacity = oldTable.length;
    if (oldCapacity == MAXIMUM_CAPACITY) {
        threshold = Integer.MAX_VALUE;
        return;
    }
    Entry[] newTable = new Entry[newCapacity];
    transfer(newTable);
    table = newTable;
    threshold = (int)(newCapacity * loadFactor);
}



/**
 * Transfers all entries from current table to newTable.
 */
void transfer(Entry[] newTable) {
    Entry[] src = table;
    int newCapacity = newTable.length;
    for (int j = 0; j < src.length; j++) {
        Entry<K,V> e = src[j];
        if (e != null) {
            src[j] = null;
            do {
                Entry<K,V> next = e.next;
                //重新計算index
                int i = indexFor(e.hash, newCapacity);
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            } while (e != null);
        }
    }
}

原文鏈接:http://www.hollischuang.com/archives/82

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末菌羽,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子由缆,更是在濱河造成了極大的恐慌注祖,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,104評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件均唉,死亡現(xiàn)場離奇詭異是晨,居然都是意外死亡,警方通過查閱死者的電腦和手機舔箭,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,816評論 3 399
  • 文/潘曉璐 我一進店門罩缴,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人层扶,你說我怎么就攤上這事箫章。” “怎么了镜会?”我有些...
    開封第一講書人閱讀 168,697評論 0 360
  • 文/不壞的土叔 我叫張陵檬寂,是天一觀的道長。 經(jīng)常有香客問我戳表,道長桶至,這世上最難降的妖魔是什么昼伴? 我笑而不...
    開封第一講書人閱讀 59,836評論 1 298
  • 正文 為了忘掉前任,我火速辦了婚禮镣屹,結(jié)果婚禮上圃郊,老公的妹妹穿的比我還像新娘。我一直安慰自己野瘦,他們只是感情好描沟,可當(dāng)我...
    茶點故事閱讀 68,851評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著鞭光,像睡著了一般。 火紅的嫁衣襯著肌膚如雪泞遗。 梳的紋絲不亂的頭發(fā)上惰许,一...
    開封第一講書人閱讀 52,441評論 1 310
  • 那天,我揣著相機與錄音史辙,去河邊找鬼汹买。 笑死,一個胖子當(dāng)著我的面吹牛聊倔,可吹牛的內(nèi)容都是我干的晦毙。 我是一名探鬼主播,決...
    沈念sama閱讀 40,992評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼耙蔑,長吁一口氣:“原來是場噩夢啊……” “哼见妒!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起甸陌,我...
    開封第一講書人閱讀 39,899評論 0 276
  • 序言:老撾萬榮一對情侶失蹤须揣,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后钱豁,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,457評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,529評論 3 341
  • 正文 我和宋清朗相戀三年歉糜,在試婚紗的時候發(fā)現(xiàn)自己被綠了戳玫。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,664評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡谤碳,死狀恐怖溃卡,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情估蹄,我是刑警寧澤塑煎,帶...
    沈念sama閱讀 36,346評論 5 350
  • 正文 年R本政府宣布,位于F島的核電站臭蚁,受9級特大地震影響最铁,放射性物質(zhì)發(fā)生泄漏讯赏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 42,025評論 3 334
  • 文/蒙蒙 一冷尉、第九天 我趴在偏房一處隱蔽的房頂上張望漱挎。 院中可真熱鬧,春花似錦雀哨、人聲如沸磕谅。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,511評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽膊夹。三九已至,卻和暖如春捌浩,著一層夾襖步出監(jiān)牢的瞬間放刨,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,611評論 1 272
  • 我被黑心中介騙來泰國打工尸饺, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留进统,地道東北人。 一個月前我還...
    沈念sama閱讀 49,081評論 3 377
  • 正文 我出身青樓浪听,卻偏偏與公主長得像螟碎,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子迹栓,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,675評論 2 359

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