JAVA源碼分析-HashMap源碼分析(一)

一直以來碳柱,HashMap就是Java面試過程中的彻共福客革答,不管是剛畢業(yè)的,還是工作了好多年的同學(xué)曙强,在Java面試過程中残拐,經(jīng)常會(huì)被問到HashMap相關(guān)的一些問題,而且每次面試都被問到一些自己平時(shí)沒有注意的問題旗扑。因?yàn)镠ashMap不管對(duì)于畢業(yè)生蹦骑,還是對(duì)于老司機(jī)來說,都非常熟悉臀防,熟悉到你經(jīng)常忽略它眠菇。

本著知其然,更要知其所以然的精神袱衷,本人對(duì)JDK 1.8版本的HashMap源碼進(jìn)行了仔細(xì)的學(xué)習(xí)捎废。大家知道,JDK 1.8中HashMap的實(shí)現(xiàn)有了一些改進(jìn)致燥,特別是數(shù)據(jù)存儲(chǔ)結(jié)構(gòu)引進(jìn)了紅黑樹登疗,使得查詢更加的快捷,本文也會(huì)對(duì)相應(yīng)的內(nèi)容進(jìn)行分析,希望大家能有收獲辐益。

一断傲、HashMap基礎(chǔ)

1.1 HashMap的定義

話不多說,首先從HashMap的一些基礎(chǔ)開始智政。我們先看一下HashMap的定義:

public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable

我們可以看出认罩,HashMap繼承了AbstractMap<K,V>抽象類,實(shí)現(xiàn)了Map<K,V>的方法续捂。

1.2 HashMap的屬性

接著垦垂,我們通過源碼看看HashMap的一些重要的常量屬性。

//默認(rèn)容量
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
//最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
//默認(rèn)加載因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//鏈表轉(zhuǎn)成紅黑樹的閾值
static final int TREEIFY_THRESHOLD = 8;
//紅黑樹轉(zhuǎn)為鏈表的閾值
static final int UNTREEIFY_THRESHOLD = 6;
//存儲(chǔ)方式由鏈表轉(zhuǎn)成紅黑樹的容量的最小閾值
static final int MIN_TREEIFY_CAPACITY = 64;
//HashMap中存儲(chǔ)的鍵值對(duì)的數(shù)量
transient int size;
//擴(kuò)容閾值牙瓢,當(dāng)size>=threshold時(shí)劫拗,就會(huì)擴(kuò)容
int threshold;
//HashMap的加載因子
final float loadFactor;

這里我們要知道<<運(yùn)算符的意義,表示移位操作矾克,每次向左移動(dòng)一位(相對(duì)于二進(jìn)制來說)页慷,表示乘以2,此處1<<4表示00001中的1向左移動(dòng)了4位聂渊,變成了10000差购,換算成十進(jìn)制就是2^4=16,也就是HashMap的默認(rèn)容量就是16汉嗽。Java中還有一些位操作符欲逃,比如類似的>>(右移),還有>>>(無符號(hào)右移)等饼暑,也是需要我們掌握的稳析。這些位操作符的計(jì)算速度很快,我們?cè)谄綍r(shí)的工作中可以使用它們來提升我們系統(tǒng)的性能弓叛。

這里我們需要加載因子(load_factor)彰居,加載因子默認(rèn)為0.75,當(dāng)HashMap中存儲(chǔ)的元素的數(shù)量大于(容量×加載因子)撰筷,也就是默認(rèn)大于16*0.75=12時(shí)陈惰,HashMap會(huì)進(jìn)行擴(kuò)容的操作。

二毕籽、初始化

一般來說抬闯,我們初始化的時(shí)候會(huì)這樣寫:

Map<K,V> map = new HashMap<K,V>();

這個(gè)過程發(fā)生了什么呢?我們看看源碼关筒。

public HashMap(int initialCapacity, float loadFactor) {
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " +
                                           initialCapacity);
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " +
                                           loadFactor);
    this.loadFactor = loadFactor;
    this.threshold = tableSizeFor(initialCapacity);
}

我們debug跟蹤時(shí)溶握,會(huì)發(fā)現(xiàn),這里的initialCapacity并不是我們想象的16蒸播,而是31睡榆,并且會(huì)變化幾次之后萍肆,initialCapacity最終變成了11,這是為什么呢胀屿?說實(shí)話塘揣,我也不清楚,希望有大神可以幫忙解答碉纳。

我們繼續(xù)勿负。初始化時(shí)馏艾,會(huì)首先判斷初始容量是否小于0劳曹,如果小于0,會(huì)拋出異常琅摩。接著铁孵,判斷初始容量是否大于最大的容量(即2^31),如果大于房资,將初始容量設(shè)置為最大初始容量蜕劝。緊接著,判斷加載因子:如果小于等于0轰异,或者不是一個(gè)數(shù)字岖沛,都會(huì)拋出異常。等這些校驗(yàn)完成之后搭独,會(huì)將HashMap的加載因子和擴(kuò)容的閾值設(shè)置上婴削。這里需要注意一下,threshold(閾值)=capacity*loadFactor牙肝。而我們的閾值是怎么來的呢唉俗?我們看一下tableSizeFor()這個(gè)方法。

static final int tableSizeFor(int cap) {
    int n = cap - 1;
    n |= n >>> 1;
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

我們可以看到英文注釋:Returns a power of two size for the given target capacity.(返回目標(biāo)容量對(duì)應(yīng)的2的冪次方配椭。)我們可以想象一下虫溜,如果我們將初始值設(shè)置為非2的冪次方的數(shù)值,比如我們?cè)O(shè)置為19股缸,最終我們通過這個(gè)方法衡楞,得到的數(shù)組大小是多少呢?我們可以計(jì)算一下敦姻。

cap=19
int n=cap-1;//得到n=18瘾境,換算為二進(jìn)制為10010
n|=n>>>1;//表示n無符號(hào)右移一位后,與n按位或計(jì)算替劈,其中n>>>1=01001寄雀,按位或結(jié)果為11011
n|=n>>>2;//其中n>>>2=00110,按位或的結(jié)果為11111,下面幾步類似陨献,最終得到的結(jié)果是n=11111(二進(jìn)制盒犹,也就是2^5-1,31)

最終計(jì)算得到的結(jié)果是32

因?yàn)閏ap最大為2^31,我們可以知道急膀,這個(gè)方法的最終目的就是返回比cap大的最小的2的冪次方沮协。

三、put()

下面卓嫂,我們開始解析HashMap中最重要的一個(gè)方法:put()慷暂。

//如果原來存在相同的key-value,原來的value會(huì)被替換掉
public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

下面我們首先看一下hash(key)晨雳,然后再看一下putVal()方法行瑞,這兩個(gè)方法是精髓。

3.1 hash(key)

先上源碼:

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

我們可以發(fā)現(xiàn)餐禁,當(dāng)key=null時(shí)血久,也是有hash值的,是0帮非,所以氧吐,HashMap的key是可以為null的,對(duì)比HashTable源碼我們可以知道末盔,HashTable的key直接進(jìn)行了hashCode筑舅,如果key為null時(shí),會(huì)拋出異常陨舱,所以HashTable的key不可以是null翠拣。

我們還能發(fā)現(xiàn)hash值的計(jì)算,首先計(jì)算出key的hashCode()為h隅忿,然后與h無條件右移16位后的二進(jìn)制進(jìn)行按位異或(^)得到最終的hash值心剥,這個(gè)hash值就是鍵值對(duì)存儲(chǔ)在數(shù)組中的位置。

備注:異或的操作如下:0 ^ 0=0背桐,1 ^ 1 =0优烧,0 ^ 1=1,1 ^ 0=1链峭,也就是相同時(shí)返回0畦娄,不同時(shí)返回1。

我們目前不去深究為什么這么設(shè)計(jì)弊仪,我們只要知道熙卡,這樣設(shè)計(jì)的目的是為了讓hash值分布的更加均勻即可。

3.2 putVal()方法

3.2.1 源碼

我們直接看源碼励饵。

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;
        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);
                    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;
}

我們慢慢來分析驳癌。首先看入?yún)ⅲ?/p>

  • hash:表示key的hash值
  • key:待存儲(chǔ)的key值
  • value:待存儲(chǔ)的value值,從這個(gè)方法可以知道,HashMap底層存儲(chǔ)的是key-value的鍵值對(duì),不只是存儲(chǔ)了value
  • onlyIfAbsent:這個(gè)參數(shù)表示,是否需要替換相同的value值堕油,如果為true甜滨,表示不替換已經(jīng)存在的value
  • evict:如果為false乐严,表示數(shù)組是新增模式

我們看到put時(shí)所傳入的參數(shù)put(hash(key), key, value, false, true),可以得到相應(yīng)的含義衣摩。

3.2.2 HashMap的數(shù)據(jù)結(jié)構(gòu)

在繼續(xù)下一步分析之前昂验,我們首先需要看一下HashMap底層的數(shù)據(jù)結(jié)構(gòu)。

HashMap的數(shù)據(jù)結(jié)構(gòu)

我們可以看到艾扮,HashMap底層是數(shù)組加單向鏈表或紅黑樹實(shí)現(xiàn)的(這是JDK 1.8里面的內(nèi)容既琴,之前的版本純粹是數(shù)組加單向鏈表實(shí)現(xiàn))。

下面我們看一下HashMap的一些重要的內(nèi)部類栏渺。首先最重要的就是Node類呛梆,即HashMap內(nèi)部定義的單向鏈表

static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    V value;
    Node<K,V> next;
    
    //省略一些代碼

    public final int hashCode() {
        return Objects.hashCode(key) ^ Objects.hashCode(value);
    }

    public final V setValue(V newValue) {
        V oldValue = value;
        value = newValue;
        return oldValue;
    }

    public final boolean equals(Object o) {
        if (o == this)
            return true;
        if (o instanceof Map.Entry) {
            Map.Entry<?,?> e = (Map.Entry<?,?>)o;
            if (Objects.equals(key, e.getKey()) &&
                Objects.equals(value, e.getValue()))
                return true;
        }
        return false;
    }
}

我們重點(diǎn)看一下數(shù)據(jù)結(jié)構(gòu),Node中存儲(chǔ)了key的hash值磕诊,鍵值對(duì),同時(shí)還有下一個(gè)鏈表元素纹腌。我們重點(diǎn)關(guān)注一些equals這個(gè)方法霎终,這個(gè)方法在什么時(shí)候會(huì)用到呢?當(dāng)我們算出的key的hash值相同時(shí)升薯,put方法并不會(huì)報(bào)錯(cuò)莱褒,而是繼續(xù)向這個(gè)hash值的鏈表中添加元素。我們會(huì)調(diào)用equals方法來比對(duì)key和value是否相同涎劈,如果equals方法返回false广凸,會(huì)繼續(xù)向鏈表的尾部添加一個(gè)鍵值對(duì)。

當(dāng)然蛛枚,在JDK 1.8中引入了紅黑樹的概念谅海,內(nèi)部定義為TreeNode,對(duì)紅黑樹感興趣的同學(xué)可以看看相關(guān)的文檔蹦浦,引入紅黑樹是為了提升查詢的效率扭吁。

3.2.3 繼續(xù)分析putVal()方法

首先判斷當(dāng)前HashMap的數(shù)組是否為空,如果為空盲镶,則調(diào)用resize()方法侥袜,對(duì)HashMap進(jìn)行擴(kuò)容,這次擴(kuò)容的結(jié)果就是HashMap的初始化一個(gè)長度為16的數(shù)組溉贿。獲取到數(shù)組的長度n枫吧。代碼如下:

if ((tab = table) == null || (n = tab.length) == 0)
    n = (tab = resize()).length;

接著,根據(jù)長度-1和hash值進(jìn)行按位與運(yùn)算宇色,算出hash值對(duì)應(yīng)于數(shù)組中的位置九杂,從tab中將這個(gè)位置上面的內(nèi)容取出闽寡,判斷為null時(shí),在這個(gè)位置新增一個(gè)Node尼酿。代碼如下:

if ((p = tab[i = (n - 1) & hash]) == null)
    tab[i] = newNode(hash, key, value, null);
    
// Create a regular (non-tree) node
Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) {
    return new Node<>(hash, key, value, next);
}

如果同樣的位置取到了數(shù)據(jù)爷狈,也就是這個(gè)hash值對(duì)應(yīng)數(shù)組的位置上面已經(jīng)有了鍵值對(duì)存在,這時(shí)候我們就需要做一些動(dòng)作了裳擎。首先涎永,我們判斷這個(gè)Node,也就是p的hash值是否與傳入的hash相等鹿响,然后接著判斷key是否相等(這里判斷key是否相等羡微,用了一個(gè)或運(yùn)算)。如果判斷通過惶我,表示要傳入的key-val鍵值對(duì)就是tab[i]位置上面的鍵值對(duì)妈倔,直接替換即可,不用管后面是鏈表還是紅黑樹绸贡。代碼如下:

Node<K,V> e; K k;
if (p.hash == hash &&
    ((k = p.key) == key || (key != null && key.equals(k))))
    e = p;

如果tab[i]的key不是我們傳入的key盯蝴,下面我們首先要判斷p這個(gè)Node是不是紅黑樹,如果是紅黑樹听怕,直接向紅黑樹新增一個(gè)數(shù)據(jù)捧挺。向紅黑樹新增數(shù)據(jù)的代碼我們后續(xù)再解析,目前先不進(jìn)行分析尿瞭。代碼如下:

else if (p instanceof TreeNode)
    e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);

下面闽烙,當(dāng)p是單向鏈表時(shí),我們遍歷鏈表進(jìn)行插入等操作声搁。找到鏈表的尾部黑竞,將節(jié)點(diǎn)新增到尾部。如果鏈表的長度大于等于紅黑樹化的閾值-1疏旨,就將桶(也就是鏈表)轉(zhuǎn)成紅黑樹存儲(chǔ)數(shù)據(jù)很魂。如果在鏈表中還存在相同的key,直接替換舊的value即可充石。

    for (int binCount = 0; ; ++binCount) {
        if ((e = p.next) == null) {
            p.next = newNode(hash, key, value, null);
            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;
}

最后莫换,還有一個(gè)操作,大家千萬不要忽略骤铃,也就是判斷當(dāng)前的鍵值對(duì)數(shù)量是否即將超過閾值拉岁,如果即將超過,需要進(jìn)行resize()操作惰爬。

if (++size > threshold)
    resize();

下一篇文章我們將著重分析resize()和get()的源碼喊暖。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市撕瞧,隨后出現(xiàn)的幾起案子陵叽,更是在濱河造成了極大的恐慌狞尔,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,214評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件巩掺,死亡現(xiàn)場離奇詭異偏序,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)胖替,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門研儒,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人独令,你說我怎么就攤上這事端朵。” “怎么了燃箭?”我有些...
    開封第一講書人閱讀 152,543評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵冲呢,是天一觀的道長。 經(jīng)常有香客問我招狸,道長敬拓,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,221評(píng)論 1 279
  • 正文 為了忘掉前任瓢颅,我火速辦了婚禮恩尾,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘挽懦。我一直安慰自己,他們只是感情好木人,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,224評(píng)論 5 371
  • 文/花漫 我一把揭開白布信柿。 她就那樣靜靜地躺著,像睡著了一般醒第。 火紅的嫁衣襯著肌膚如雪渔嚷。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,007評(píng)論 1 284
  • 那天稠曼,我揣著相機(jī)與錄音形病,去河邊找鬼。 笑死霞幅,一個(gè)胖子當(dāng)著我的面吹牛漠吻,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播司恳,決...
    沈念sama閱讀 38,313評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼途乃,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了扔傅?” 一聲冷哼從身側(cè)響起耍共,我...
    開封第一講書人閱讀 36,956評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤烫饼,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后试读,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體杠纵,經(jīng)...
    沈念sama閱讀 43,441評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,925評(píng)論 2 323
  • 正文 我和宋清朗相戀三年钩骇,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了比藻。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,018評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡伊履,死狀恐怖韩容,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情唐瀑,我是刑警寧澤群凶,帶...
    沈念sama閱讀 33,685評(píng)論 4 322
  • 正文 年R本政府宣布,位于F島的核電站哄辣,受9級(jí)特大地震影響请梢,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜力穗,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,234評(píng)論 3 307
  • 文/蒙蒙 一毅弧、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧当窗,春花似錦够坐、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至巫员,卻和暖如春庶香,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背简识。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評(píng)論 1 261
  • 我被黑心中介騙來泰國打工赶掖, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人七扰。 一個(gè)月前我還...
    沈念sama閱讀 45,467評(píng)論 2 352
  • 正文 我出身青樓奢赂,卻偏偏與公主長得像,于是被迫代替她去往敵國和親戳寸。 傳聞我的和親對(duì)象是個(gè)殘疾皇子呈驶,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,762評(píng)論 2 345

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

  • 一袖瞻、基本數(shù)據(jù)類型 注釋 單行注釋:// 區(qū)域注釋:/* */ 文檔注釋:/** */ 數(shù)值 對(duì)于byte類型而言...
    龍貓小爺閱讀 4,254評(píng)論 0 16
  • HashMap 是 Java 面試必考的知識(shí)點(diǎn)司致,面試官從這個(gè)小知識(shí)點(diǎn)就可以了解我們對(duì) Java 基礎(chǔ)的掌握程度。網(wǎng)...
    野狗子嗷嗷嗷閱讀 6,657評(píng)論 9 107
  • 前言 這次我和大家一起學(xué)習(xí)HashMap聋迎,HashMap我們?cè)诠ぷ髦薪?jīng)常會(huì)使用脂矫,而且面試中也很頻繁會(huì)問到,因?yàn)樗?..
    liangzzz閱讀 7,965評(píng)論 7 102
  • 人一生的時(shí)間多么短暫而寶貴霉晕?可我們卻把四分之一到三分之一的時(shí)間都用來睡覺庭再。不得不說,睡覺是人生大事牺堰。 不過現(xiàn)在很多...
    NiceNeil閱讀 487評(píng)論 0 0
  • 四月拄轻,穿紅著綠 散發(fā)著淡淡的花草氣息 經(jīng)風(fēng)沐雨 搖動(dòng)著思念的經(jīng)幡 趕赴一場天地的清明 人生代謝成塚 花開...
    綠羽清寧閱讀 220評(píng)論 2 2