HashMap的原理

前言

本篇文章我們將帶領大家一起看看HashMap的源碼涉瘾,從源碼角度出發(fā)溺欧,看看HashMap里面的數(shù)據(jù)結構是這么樣的,怎么往HashMap里面put和get數(shù)據(jù)色迂,什么是HashMap指針碰撞等等

在JDK 1.7 Android 24之前(包括)屉栓,HashMap的數(shù)據(jù)結構是數(shù)組+鏈表</br>

在JDK 1.8 Android 25之后俺夕,HashMap的數(shù)據(jù)結構是數(shù)組+鏈表+紅黑樹</br>

HashMap的put()方法

接下來我們一起看下Android 24里面HashMap的源碼
我們往HashMap里面put元素的時候都有哪些操作呢促王?

public V put(K key, V value) {
    if (table == EMPTY_TABLE) {
        inflateTable(threshold);
    }
    if (key == null)
        return putForNullKey(value);
    int hash = sun.misc.Hashing.singleWordWangJenkinsHash(key);
    int i = indexFor(hash, table.length);
    for (HashMapEntry<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;
}

首先我們會拿到key對應的一個hash值瞒大,indexFor()方法會拿到一個table數(shù)組的索引i系洛,i的值是hash值與數(shù)組長度-1之后的與操作

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

key value一一對應

在開發(fā)中經(jīng)常會重復往同一個key里面put幾個值俊性,HashMap是如何保證key value的一對一映射關系的

for (HashMapEntry<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;
    }
}

如果當前index對應的鏈表有數(shù)據(jù),就循環(huán)遍歷描扯,如果key值存在定页,將新傳進來的value復制給e的vlue
如果不存在就要去重新創(chuàng)建了。</br>

碰撞問題

key是唯一的绽诚,key對應的hashcode也唯一典徊,但是hashcode與length-1的與結果不唯一。當兩個不同的key的hashcode & length -1 相同的時候就會出現(xiàn)常說的hash碰撞問題恩够。哈希表是如何解決碰撞問題的呢宫峦?一起看下源碼。</br>
addEntry() -> createEntry()

void createEntry(int hash, K key, V value, int bucketIndex) {
    HashMapEntry<K,V> e = table[bucketIndex];
    table[bucketIndex] = new HashMapEntry<>(hash, key, value, e);
    size++;
}

會把key value作為新的鏈表的key和value玫鸟,之前的鏈表作為當前鏈表的next節(jié)點导绷,然后生成新的鏈表重新賦值給數(shù)組對應的元素。這就是我們常說的HashMap的碰撞問題屎飘。

HashMap就是采用頭插法解決hash沖突的妥曲。
hashmap1.png

get()方法

public V get(Object key) {
    if (key == null)
        return getForNullKey();
    Entry<K,V> entry = getEntry(key);

    return null == entry ? null : entry.getValue();
}

final Entry<K,V> getEntry(Object key) {
    if (size == 0) {
        return null;
    }

    int hash = (key == null) ? 0 : sun.misc.Hashing.singleWordWangJenkinsHash(key);
    for (HashMapEntry<K,V> e = table[indexFor(hash, table.length)];
         e != null;
         e = e.next) {
        Object k;
        if (e.hash == hash &&
            ((k = e.key) == key || (key != null && key.equals(k))))
            return e;
    }
    return null;
}

前面了解了怎么存數(shù)據(jù)的,取數(shù)據(jù)就容易理解了钦购。通過key算出hash值和index檐盟,找到對應的table[index],遍歷鏈表找到value押桃。</br>

擴容

當hashmap所有的put操作都沖突的時候葵萎,即所有的元素都在一個table[index]里面,這個時候hashmap退化成了單鏈表唱凯,效率最低羡忘。</br>
為了盡量避免這種情況,hashmap需要擴容磕昼。length越大卷雕,沖突的概率就越低了。</br>

加載因子 DEFAULT_LOAD_FACTOR 默認 0.75

HashMap創(chuàng)建的時候如果不設置的話票从,默認的大小是16漫雕。

/**
 * The default initial capacity - MUST be a power of two.
 */
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
閾值 = DEFAULT_LOAD_FACTOR * CAPACITY 默認是12

當hashmap的元素大于閾值時就會擴容滨嘱。

void addEntry(int hash, K key, V value, int bucketIndex) {
    if ((size >= threshold) && (null != table[bucketIndex])) {
        resize(2 * table.length);
        hash = (null != key) ? sun.misc.Hashing.singleWordWangJenkinsHash(key) : 0;
        bucketIndex = indexFor(hash, table.length);
    }

    createEntry(hash, key, value, bucketIndex);
}

void resize(int newCapacity) {
    HashMapEntry[] oldTable = table;
    int oldCapacity = oldTable.length;
    if (oldCapacity == MAXIMUM_CAPACITY) {
        threshold = Integer.MAX_VALUE;
        return;
    }

    HashMapEntry[] newTable = new HashMapEntry[newCapacity];
    transfer(newTable);
    table = newTable;
    threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}

當hashmap resize()的時候,hashmap的length發(fā)生了變化浸间,那么值錢計算的index 是錯誤的了太雨,就需要重新計算了。

/**
 * Transfers all entries from current table to newTable.
 */
void transfer(HashMapEntry[] newTable) {
    int newCapacity = newTable.length;
    for (HashMapEntry<K,V> e : table) {
        while(null != e) {
            HashMapEntry<K,V> next = e.next;
            int i = indexFor(e.hash, newCapacity);
            e.next = newTable[i];
            newTable[i] = e;
            e = next;
        }
    }
}

所以哈希表的擴容的操作是很消耗性能的魁蒜,因此我們在new HashMap(size)的時候應該將size傳給構造函數(shù)囊扳,大小為 hashmap所有元素的大小/0.75 + 1</br>
在put()方法最開始我們還有一個地方?jīng)]講

if (table == EMPTY_TABLE) {
    inflateTable(threshold);
}

/**
 * Inflates the table.
 */
private void inflateTable(int toSize) {
    // Find a power of 2 >= toSize
    int capacity = roundUpToPowerOf2(toSize);

    // Android-changed: Replace usage of Math.min() here because this method is
    // called from the <clinit> of runtime, at which point the native libraries
    // needed by Float.* might not be loaded.
    float thresholdFloat = capacity * loadFactor;
    if (thresholdFloat > MAXIMUM_CAPACITY + 1) {
        thresholdFloat = MAXIMUM_CAPACITY + 1;
    }

    threshold = (int) thresholdFloat;
    table = new HashMapEntry[capacity];
}

 private static int roundUpToPowerOf2(int number) {
    // assert number >= 0 : "number must be non-negative";
    int rounded = number >= MAXIMUM_CAPACITY
            ? MAXIMUM_CAPACITY
            : (rounded = Integer.highestOneBit(number)) != 0
                ? (Integer.bitCount(number) > 1) ? rounded << 1 : rounded
                : 1;

    return rounded;
}

這個方法主要是初始化哈希表,計算哈希表的大忻饭摺(必須為2的冪)</br>

結尾給大家留個問題宪拥,為什么哈希表的大小一定要是2的次冪?大家可以試試自己舉個例子試試铣减,如果不是2的次冪她君,碰撞的概率會明顯增加,造成了空間浪費葫哗。
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末缔刹,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子劣针,更是在濱河造成了極大的恐慌校镐,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,378評論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件捺典,死亡現(xiàn)場離奇詭異鸟廓,居然都是意外死亡,警方通過查閱死者的電腦和手機襟己,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,970評論 3 399
  • 文/潘曉璐 我一進店門引谜,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人擎浴,你說我怎么就攤上這事员咽。” “怎么了贮预?”我有些...
    開封第一講書人閱讀 168,983評論 0 362
  • 文/不壞的土叔 我叫張陵贝室,是天一觀的道長。 經(jīng)常有香客問我仿吞,道長滑频,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,938評論 1 299
  • 正文 為了忘掉前任茫藏,我火速辦了婚禮误趴,結果婚禮上,老公的妹妹穿的比我還像新娘务傲。我一直安慰自己凉当,他們只是感情好,可當我...
    茶點故事閱讀 68,955評論 6 398
  • 文/花漫 我一把揭開白布售葡。 她就那樣靜靜地躺著看杭,像睡著了一般。 火紅的嫁衣襯著肌膚如雪挟伙。 梳的紋絲不亂的頭發(fā)上楼雹,一...
    開封第一講書人閱讀 52,549評論 1 312
  • 那天,我揣著相機與錄音尖阔,去河邊找鬼贮缅。 笑死,一個胖子當著我的面吹牛介却,可吹牛的內容都是我干的谴供。 我是一名探鬼主播,決...
    沈念sama閱讀 41,063評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼齿坷,長吁一口氣:“原來是場噩夢啊……” “哼桂肌!你這毒婦竟也來了?” 一聲冷哼從身側響起永淌,我...
    開封第一講書人閱讀 39,991評論 0 277
  • 序言:老撾萬榮一對情侶失蹤崎场,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后遂蛀,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體谭跨,經(jīng)...
    沈念sama閱讀 46,522評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 38,604評論 3 342
  • 正文 我和宋清朗相戀三年李滴,在試婚紗的時候發(fā)現(xiàn)自己被綠了螃宙。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,742評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡悬嗓,死狀恐怖污呼,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情包竹,我是刑警寧澤燕酷,帶...
    沈念sama閱讀 36,413評論 5 351
  • 正文 年R本政府宣布,位于F島的核電站周瞎,受9級特大地震影響苗缩,放射性物質發(fā)生泄漏。R本人自食惡果不足惜声诸,卻給世界環(huán)境...
    茶點故事閱讀 42,094評論 3 335
  • 文/蒙蒙 一酱讶、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧彼乌,春花似錦泻肯、人聲如沸渊迁。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,572評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽琉朽。三九已至,卻和暖如春稚铣,著一層夾襖步出監(jiān)牢的瞬間箱叁,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,671評論 1 274
  • 我被黑心中介騙來泰國打工惕医, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留耕漱,地道東北人。 一個月前我還...
    沈念sama閱讀 49,159評論 3 378
  • 正文 我出身青樓抬伺,卻偏偏與公主長得像螟够,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子沛简,可洞房花燭夜當晚...
    茶點故事閱讀 45,747評論 2 361