1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

        面試再問 HashMap,求你把這篇文章發(fā)給他!

        共 796字,需瀏覽 2分鐘

         ·

        2020-09-10 22:04

        點擊上方“碼農(nóng)突圍”,馬上關(guān)注
        這里是碼農(nóng)充電第一站,回復(fù)“666”,獲取一份專屬大禮包
        真愛,請設(shè)置“星標”或點個“在看”
        來源:sf.gg/a/1190000022184751
        • 數(shù)據(jù)結(jié)構(gòu)
        • table數(shù)組長度永遠為2的冪次方
        • 擴容
        • 查找
        • 插入
        • 刪除
        • 遍歷
        • equasl和hashcode
        • 總結(jié)

        HashMap是面試中經(jīng)常問到的一個知識點,也是判斷一個候選人基礎(chǔ)是否扎實的標準之一,因為通過HashMap可以引出很多知識點,比如數(shù)據(jù)結(jié)構(gòu)(數(shù)組、鏈表、紅黑樹)、equals和hashcode方法,除此之外還可以引出線程安全的問題,HashMap是我在初學(xué)階段學(xué)到的設(shè)計的最為巧妙的集合,里面有很多細節(jié)以及優(yōu)化技巧都值得我們深入學(xué)習(xí),本文將會涉及到以下問題
        • 默認大小、負載因子以及擴容倍數(shù)
        • 底層數(shù)據(jù)結(jié)構(gòu)
        • 如何處理hash沖突
        • 如何計算key的hash值
        • 數(shù)組長度為什么是2的冪次方
        • 查找、插入、擴容過程
        • fail-fast機制
        如果上面的都能回答出來的話那么這篇文章可能不太適合你,話不多說進入正文。注意:本文源碼都是以JDK1.8版本講解

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

        • 在 JDK1.8 中,HashMap 是由 數(shù)組+鏈表+紅黑樹構(gòu)成(1.7版本是數(shù)組+鏈表)
        • 當一個值中要存儲到HashMap中的時候會根據(jù)Key的值來計算出他的hash,通過hash值來確認存放到數(shù)組中的位置,如果發(fā)生hash沖突就以鏈表的形式存儲,當鏈表過長的話,HashMap會把這個鏈表轉(zhuǎn)換成紅黑樹來存儲,如圖所示:
        在看源碼之前我們需要先看看一些基本屬性
        //默認初始容量為16
        static?final?int?DEFAULT_INITIAL_CAPACITY?=?1?<4;
        //默認負載因子為0.75
        static?final?float?DEFAULT_LOAD_FACTOR?=?0.75f;
        //Hash數(shù)組(在resize()中初始化)
        transient?Node[]?table;
        //元素個數(shù)
        transient?int?size;
        //容量閾值(元素個數(shù)大于等于該值時會自動擴容)
        int?threshold;
        table數(shù)組里面存放的是Node對象,Node是HashMap的一個內(nèi)部類,用來表示一個key-value,源碼如下:
        static?class?Node<K,V>?implements?Map.Entry<K,V>?{
        ????final?int?hash;
        ????final?K?key;
        ????V?value;
        ????Node?next;

        ????Node(int?hash,?K?key,?V?value,?Node?next)?{
        ????????this.hash?=?hash;
        ????????this.key?=?key;
        ????????this.value?=?value;
        ????????this.next?=?next;
        ????}

        ????public?final?K?getKey()????????{?return?key;?}
        ????public?final?V?getValue()??????{?return?value;?}
        ????public?final?String?toString()?{?return?key?+?"="?+?value;?}
        ????public?final?int?hashCode()?{
        ????????return?Objects.hashCode(key)?^?Objects.hashCode(value);//^表示相同返回0,不同返回1
        ????????//Objects.hashCode(o)————>return?o?!=?null???o.hashCode()?:?0;
        ????}

        ????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;
        ????????????//Objects.equals(1,b)————>?return?(a?==?b)?||?(a?!=?null?&&?a.equals(b));
        ????????????if?(Objects.equals(key,?e.getKey())?&&?Objects.equals(value,?e.getValue()))
        ????????????????return?true;
        ????????}
        ????????return?false;
        ????}
        }
        總結(jié)
        • 默認初始容量為16,默認負載因子為0.75
        • threshold = 數(shù)組長度 * loadFactor,當元素個數(shù)大于等于threshold(容量閾值)時,HashMap會進行擴容操作
        • table數(shù)組中存放指向鏈表的引用
        這里需要注意的一點是table數(shù)組并不是在構(gòu)造方法里面初始化的,它是在resize(擴容)方法里進行初始化的。
        這里說句題外話:可能有刁鉆的面試官會問為什么默認初始容量要設(shè)置為16?為什么負載因子要設(shè)置為0.75?我們都知道HashMap數(shù)組長度被設(shè)計成2的冪次方(下面會講),那為什么初始容量不設(shè)計成4、8或者32.... 其實這是JDK設(shè)計者經(jīng)過權(quán)衡之后得出的一個比較合理的數(shù)字,,如果默認容量是8的話,當添加到第6個元素的時候就會觸發(fā)擴容操作,擴容操作是非常消耗CPU的,32的話如果只添加少量元素則會浪費內(nèi)存,因此設(shè)計成16是比較合適的,負載因子也是同理。

        table數(shù)組長度永遠為2的冪次方

        總所周知,HashMap數(shù)組長度永遠為2的冪次方(指的是table數(shù)組的大小),那你有想過為什么嗎?
        首先我們需要知道HashMap是通過一個名為tableSizeFor的方法來確保HashMap數(shù)組長度永遠為2的冪次方的,源碼如下:
        /*找到大于或等于?cap?的最小2的冪,用來做容量閾值*/
        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;
        }
        tableSizeFor的功能(不考慮大于最大容量的情況)是返回大于等于輸入?yún)?shù)且最近的2的整數(shù)次冪的數(shù)。比如10,則返回16。
        該算法讓最高位的1后面的位全變?yōu)?。最后再讓結(jié)果n+1,即得到了2的整數(shù)次冪的值了。
        cap-1再賦值給n的目的是另找到的目標值大于或等于原值。例如二進制1000,十進制數(shù)值為8。如果不對它減1而直接操作,將得到答案10000,即16。顯然不是結(jié)果。減1后二進制為111,再進行操作則會得到原來的數(shù)值1000,即8。通過一系列位運算大大提高效率。
        那在什么地方會用到tableSizeFor方法呢?
        答案就是在構(gòu)造方法里面調(diào)用該方法來設(shè)置threshold,也就是容量閾值。
        這里你可能又會有一個疑問:為什么要設(shè)置為threshold呢?
        因為在擴容方法里第一次初始化table數(shù)組時會將threshold設(shè)置數(shù)組的長度,后續(xù)在講擴容方法時再介紹。
        /*傳入初始容量和負載因子*/
        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);
        }

        那么為什么要把數(shù)組長度設(shè)計為2的冪次方呢?

        我個人覺得這樣設(shè)計有以下幾個好處:
        1、當數(shù)組長度為2的冪次方時,可以使用位運算來計算元素在數(shù)組中的下標
        HashMap是通過index=hash&(table.length-1)這條公式來計算元素在table數(shù)組中存放的下標,就是把元素的hash值和數(shù)組長度減1的值做一個與運算,即可求出該元素在數(shù)組中的下標,這條公式其實等價于hash%length,也就是對數(shù)組長度求模取余,只不過只有當數(shù)組長度為2的冪次方時,hash&(length-1)才等價于hash%length,使用位運算可以提高效率。
        2、 增加hash值的隨機性,減少hash沖突
        如果 length 為 2 的冪次方,則 length-1 轉(zhuǎn)化為二進制必定是 11111……的形式,這樣的話可以使所有位置都能和元素hash值做與運算,如果是如果 length 不是2的次冪,比如length為15,則length-1為14,對應(yīng)的二進制為1110,在和hash 做與運算時,最后一位永遠都為0 ,浪費空間。

        擴容

        HashMap每次擴容都是建立一個新的table數(shù)組,長度和容量閾值都變?yōu)樵瓉淼膬杀?,然后把原?shù)組元素重新映射到新數(shù)組上,具體步驟如下:
        1. 首先會判斷table數(shù)組長度,如果大于0說明已被初始化過,那么按當前table數(shù)組長度的2倍進行擴容,閾值也變?yōu)樵瓉淼?倍
        2. 若table數(shù)組未被初始化過,且threshold(閾值)大于0說明調(diào)用了HashMap(initialCapacity, loadFactor)構(gòu)造方法,那么就把數(shù)組大小設(shè)為threshold
        3. 若table數(shù)組未被初始化,且threshold為0說明調(diào)用HashMap()構(gòu)造方法,那么就把數(shù)組大小設(shè)為16,threshold設(shè)為16*0.75
        4. 接著需要判斷如果不是第一次初始化,那么擴容之后,要重新計算鍵值對的位置,并把它們移動到合適的位置上去,如果節(jié)點是紅黑樹類型的話則需要進行紅黑樹的拆分。
        這里有一個需要注意的點就是在JDK1.8 HashMap擴容階段重新映射元素時不需要像1.7版本那樣重新去一個個計算元素的hash值,而是通過hash & oldCap的值來判斷,若為0則索引位置不變,不為0則新索引=原索引+舊數(shù)組長度,為什么呢?具體原因如下:
        因為我們使用的是2次冪的擴展(指長度擴為原來2倍),所以,元素的位置要么是在原位置,要么是在原位置再移動2次冪的位置。因此,我們在擴充HashMap的時候,不需要像JDK1.7的實現(xiàn)那樣重新計算hash,只需要看看原來的hash值新增的那個bit是1還是0就好了,是0的話索引沒變,是1的話索引變成“原索引+oldCap
        這點其實也可以看做長度為2的冪次方的一個好處,也是HashMap 1.7和1.8之間的一個區(qū)別,具體源碼如下:
        /*擴容*/
        final?Node[]?resize()?{
        ????Node[]?oldTab?=?table;
        ????int?oldCap?=?(oldTab?==?null)???0?:?oldTab.length;
        ????int?oldThr?=?threshold;
        ????int?newCap,?newThr?=?0;

        ????//1、若oldCap>0?說明hash數(shù)組table已被初始化
        ????if?(oldCap?>?0)?{
        ????????if?(oldCap?>=?MAXIMUM_CAPACITY)?{
        ????????????threshold?=?Integer.MAX_VALUE;
        ????????????return?oldTab;
        ????????}//按當前table數(shù)組長度的2倍進行擴容,閾值也變?yōu)樵瓉淼?倍
        ????????else?if?((newCap?=?oldCap?<1)?=?DEFAULT_INITIAL_CAPACITY)
        ????????????newThr?=?oldThr?<1;
        ????}//2、若數(shù)組未被初始化,而threshold>0說明調(diào)用了HashMap(initialCapacity)和HashMap(initialCapacity,?loadFactor)構(gòu)造器
        ????else?if?(oldThr?>?0)
        ????????newCap?=?oldThr;//新容量設(shè)為數(shù)組閾值
        ????else?{?//3、若table數(shù)組未被初始化,且threshold為0說明調(diào)用HashMap()構(gòu)造方法
        ????????newCap?=?DEFAULT_INITIAL_CAPACITY;//默認為16
        ????????newThr?=?(int)(DEFAULT_LOAD_FACTOR?*?DEFAULT_INITIAL_CAPACITY);//16*0.75
        ????}

        ????//若計算過程中,閾值溢出歸零,則按閾值公式重新計算
        ????if?(newThr?==?0)?{
        ????????float?ft?=?(float)newCap?*?loadFactor;
        ????????newThr?=?(newCap?float)MAXIMUM_CAPACITY??
        ??????????????????(int)ft?:?Integer.MAX_VALUE);
        ????}
        ????threshold?=?newThr;
        ????//創(chuàng)建新的hash數(shù)組,hash數(shù)組的初始化也是在這里完成的
        ????Node[]?newTab?=?(Node[])new?Node[newCap];
        ????table?=?newTab;
        ????//如果舊的hash數(shù)組不為空,則遍歷舊數(shù)組并映射到新的hash數(shù)組
        ????if?(oldTab?!=?null)?{
        ????????for?(int?j?=?0;?j?????????????Node?e;
        ????????????if?((e?=?oldTab[j])?!=?null)?{
        ????????????????oldTab[j]?=?null;//GC
        ????????????????if?(e.next?==?null)//如果只鏈接一個節(jié)點,重新計算并放入新數(shù)組
        ????????????????????newTab[e.hash?&?(newCap?-?1)]?=?e;
        ????????????????//若是紅黑樹,則需要進行拆分
        ????????????????else?if?(e?instanceof?TreeNode)
        ????????????????????((TreeNode)e).split(this,?newTab,?j,?oldCap);
        ????????????????else?{
        ????????????????????//rehash————>重新映射到新數(shù)組
        ????????????????????Node?loHead?=?null,?loTail?=?null;
        ????????????????????Node?hiHead?=?null,?hiTail?=?null;
        ????????????????????Node?next;
        ????????????????????do?{
        ????????????????????????next?=?e.next;
        ????????????????????????/*注意這里使用的是:e.hash & oldCap,若為0則索引位置不變,不為0則新索引=原索引+舊數(shù)組長度*/
        ????????????????????????if?((e.hash?&?oldCap)?==?0)?{
        ????????????????????????????if?(loTail?==?null)
        ????????????????????????????????loHead?=?e;
        ????????????????????????????else
        ????????????????????????????????loTail.next?=?e;
        ????????????????????????????loTail?=?e;
        ????????????????????????}
        ????????????????????????else?{
        ????????????????????????????if?(hiTail?==?null)
        ????????????????????????????????hiHead?=?e;
        ????????????????????????????else
        ????????????????????????????????hiTail.next?=?e;
        ????????????????????????????hiTail?=?e;
        ????????????????????????}
        ????????????????????}?while?((e?=?next)?!=?null);
        ????????????????????if?(loTail?!=?null)?{
        ????????????????????????loTail.next?=?null;
        ????????????????????????newTab[j]?=?loHead;
        ????????????????????}
        ????????????????????if?(hiTail?!=?null)?{
        ????????????????????????hiTail.next?=?null;
        ????????????????????????newTab[j?+?oldCap]?=?hiHead;
        ????????????????????}
        ????????????????}
        ????????????}
        ????????}
        ????}
        ????return?newTab;
        }
        在擴容方法里面還涉及到有關(guān)紅黑樹的幾個知識點:

        鏈表樹化

        指的就是把鏈表轉(zhuǎn)換成紅黑樹,樹化需要滿足以下兩個條件:
        • 鏈表長度大于等于8
        • table數(shù)組長度大于等于64
        為什么table數(shù)組容量大于等于64才樹化?
        因為當table數(shù)組容量比較小時,鍵值對節(jié)點 hash 的碰撞率可能會比較高,進而導(dǎo)致鏈表長度較長。這個時候應(yīng)該優(yōu)先擴容,而不是立馬樹化。

        紅黑樹拆分

        拆分就是指擴容后對元素重新映射時,紅黑樹可能會被拆分成兩條鏈表。
        由于篇幅有限,有關(guān)紅黑樹這里就不展開了。

        查找

        在看源碼之前先來簡單梳理一下查找流程:
        1. 首先通過自定義的hash方法計算出key的hash值,求出在數(shù)組中的位置
        2. 判斷該位置上是否有節(jié)點,若沒有則返回null,代表查詢不到指定的元素
        3. 若有則判斷該節(jié)點是不是要查找的元素,若是則返回該節(jié)點
        4. 若不是則判斷節(jié)點的類型,如果是紅黑樹的話,則調(diào)用紅黑樹的方法去查找元素
        5. 如果是鏈表類型,則遍歷鏈表調(diào)用equals方法去查找元素
        HashMap的查找是非常快的,要查找一個元素首先得知道key的hash值,在HashMap中并不是直接通過key的hashcode方法獲取哈希值,而是通過內(nèi)部自定義的hash方法計算哈希值,我們來看看其實現(xiàn):
        static?final?int?hash(Object?key)?{
        ????int?h;
        ????return?(key?==?null)???0?:?(h?=?key.hashCode())?^?(h?>>>?16);
        }
        (h = key.hashCode()) ^ (h >>> 16) 是為了讓高位數(shù)據(jù)與低位數(shù)據(jù)進行異或,變相的讓高位數(shù)據(jù)參與到計算中,int有 32 位,右移16位就能讓低16位和高16位進行異或,也是為了增加hash值的隨機性。
        知道如何計算hash值后我們來看看get方法
        public?V?get(Object?key)?{
        ????Node?e;
        ????return?(e?=?getNode(hash(key),?key))?==?null???null?:?e.value;//hash(key)不等于key.hashCode
        }

        final?Node?getNode(int?hash,?Object?key)?{
        ????Node[]?tab;?//指向hash數(shù)組
        ????Node?first,?e;?//first指向hash數(shù)組鏈接的第一個節(jié)點,e指向下一個節(jié)點
        ????int?n;//hash數(shù)組長度
        ????K?k;
        ????/*(n?-?1)?&?hash?————>根據(jù)hash值計算出在數(shù)組中的索引index(相當于對數(shù)組長度取模,這里用位運算進行了優(yōu)化)*/
        ????if?((tab?=?table)?!=?null?&&?(n?=?tab.length)?>?0?&&?(first?=?tab[(n?-?1)?&?hash])?!=?null)?{
        ????????//基本類型用==比較,其它用equals比較
        ????????if?(first.hash?==?hash?&&?((k?=?first.key)?==?key?||?(key?!=?null?&&?key.equals(k))))
        ????????????return?first;
        ????????if?((e?=?first.next)?!=?null)?{
        ????????????//如果first是TreeNode類型,則調(diào)用紅黑樹查找方法
        ????????????if?(first?instanceof?TreeNode)
        ????????????????return?((TreeNode)first).getTreeNode(hash,?key);
        ????????????do?{//向后遍歷
        ????????????????if?(e.hash?==?hash?&&?((k?=?e.key)?==?key?||?(key?!=?null?&&?key.equals(k))))
        ????????????????????return?e;
        ????????????}?while?((e?=?e.next)?!=?null);
        ????????}
        ????}
        ????return?null;
        }
        這里要注意的一點就是在HashMap中用 (n - 1) & hash 計算key所對應(yīng)的索引index(相當于對數(shù)組長度取模,這里用位運算進行了優(yōu)化),這點在上面已經(jīng)說過了,就不再廢話了。

        插入

        我們先來看看插入元素的步驟:
        1. 當table數(shù)組為空時,通過擴容的方式初始化table
        2. 通過計算鍵的hash值求出下標后,若該位置上沒有元素(沒有發(fā)生hash沖突),則新建Node節(jié)點插入
        3. 若發(fā)生了hash沖突,遍歷鏈表查找要插入的key是否已經(jīng)存在,存在的話根據(jù)條件判斷是否用新值替換舊值
        4. 如果不存在,則將元素插入鏈表尾部,并根據(jù)鏈表長度決定是否將鏈表轉(zhuǎn)為紅黑樹
        5. 判斷鍵值對數(shù)量是否大于等于閾值,如果是的話則進行擴容操作
        先看完上面的流程,再來看源碼會簡單很多,源碼如下:
        public?V?put(K?key,?V?value)?{
        ????return?putVal(hash(key),?key,?value,?false,?true);
        }

        final?V?putVal(int?hash,?K?key,?V?value,?boolean?onlyIfAbsent,boolean?evict)?{
        ????Node[]?tab;//指向hash數(shù)組
        ????Node?p;//初始化為table中第一個節(jié)點
        ????int?n,?i;//n為數(shù)組長度,i為索引

        ????//tab被延遲到插入新數(shù)據(jù)時再進行初始化
        ????if?((tab?=?table)?==?null?||?(n?=?tab.length)?==?0)
        ????????n?=?(tab?=?resize()).length;
        ????//如果數(shù)組中不包含Node引用,則新建Node節(jié)點存入數(shù)組中即可
        ????if?((p?=?tab[i?=?(n?-?1)?&?hash])?==?null)
        ????????tab[i]?=?newNode(hash,?key,?value,?null);//new?Node<>(hash,?key,?value,?next)
        ????else?{
        ????????Node?e;?//如果要插入的key-value已存在,用e指向該節(jié)點
        ????????K?k;
        ????????//如果第一個節(jié)點就是要插入的key-value,則讓e指向第一個節(jié)點(p在這里指向第一個節(jié)點)
        ????????if?(p.hash?==?hash?&&?((k?=?p.key)?==?key?||?(key?!=?null?&&?key.equals(k))))
        ????????????e?=?p;
        ????????//如果p是TreeNode類型,則調(diào)用紅黑樹的插入操作(注意:TreeNode是Node的子類)
        ????????else?if?(p?instanceof?TreeNode)
        ????????????e?=?((TreeNode)p).putTreeVal(this,?tab,?hash,?key,?value);
        ????????else?{
        ????????????//對鏈表進行遍歷,并用binCount統(tǒng)計鏈表長度
        ????????????for?(int?binCount?=?0;?;?++binCount)?{
        ????????????????//如果鏈表中不包含要插入的key-value,則將其插入到鏈表尾部
        ????????????????if?((e?=?p.next)?==?null)?{
        ????????????????????p.next?=?newNode(hash,?key,?value,?null);
        ????????????????????//如果鏈表長度大于或等于樹化閾值,則進行樹化操作
        ????????????????????if?(binCount?>=?TREEIFY_THRESHOLD?-?1)
        ????????????????????????treeifyBin(tab,?hash);
        ????????????????????break;
        ????????????????}
        ????????????????//如果要插入的key-value已存在則終止遍歷,否則向后遍歷
        ????????????????if?(e.hash?==?hash?&&?((k?=?e.key)?==?key?||?(key?!=?null?&&?key.equals(k))))
        ????????????????????break;
        ????????????????p?=?e;
        ????????????}
        ????????}
        ????????//如果e不為null說明要插入的key-value已存在
        ????????if?(e?!=?null)?{
        ????????????V?oldValue?=?e.value;
        ????????????//根據(jù)傳入的onlyIfAbsent判斷是否要更新舊值
        ????????????if?(!onlyIfAbsent?||?oldValue?==?null)
        ????????????????e.value?=?value;
        ????????????afterNodeAccess(e);
        ????????????return?oldValue;
        ????????}
        ????}
        ????++modCount;
        ????//鍵值對數(shù)量大于等于閾值時,則進行擴容
        ????if?(++size?>?threshold)
        ????????resize();
        ????afterNodeInsertion(evict);//也是空函數(shù)?回調(diào)?不知道干嘛的
        ????return?null;
        }
        從源碼也可以看出table數(shù)組是在第一次調(diào)用put方法后才進行初始化的。這里還有一個知識點就是在JDK1.8版本HashMap是在鏈表尾部插入元素的,而在1.7版本里是插入鏈表頭部的,1.7版本這么設(shè)計的原因可能是作者認為新插入的元素使用到的頻率會比較高,插入頭部的話可以減少遍歷次數(shù)。
        那為什么1.8改成尾插法了呢?主要是因為頭插法在多線程環(huán)境下可能會導(dǎo)致兩個節(jié)點互相引用,形成死循環(huán),由于此文主要講解1.8版本,感興趣的小伙伴可以去看看1.7版本的源碼。

        刪除

        HashMap的刪除操作并不復(fù)雜,僅需三個步驟即可完成。
        1. 定位桶位置
        2. 遍歷鏈表找到相等的節(jié)點
        3. 第三步刪除節(jié)點
        public?V?remove(Object?key)?{
        ????Node?e;
        ????return?(e?=?removeNode(hash(key),?key,?null,?false,?true))?==?null???null?:?e.value;
        }

        final?Node?removeNode(int?hash,?Object?key,?Object?value,boolean?matchValue,?boolean?movable)?{
        ????Node[]?tab;
        ????Node?p;
        ????int?n,?index;
        ????//1、定位元素桶位置
        ????if?((tab?=?table)?!=?null?&&?(n?=?tab.length)?>?0?&&?(p?=?tab[index?=?(n?-?1)?&?hash])?!=?null)?{
        ????????Node?node?=?null,?e;
        ????????K?k;
        ????????V?v;
        ????????//?如果鍵的值與鏈表第一個節(jié)點相等,則將?node?指向該節(jié)點
        ????????if?(p.hash?==?hash?&&?((k?=?p.key)?==?key?||?(key?!=?null?&&?key.equals(k))))
        ????????????node?=?p;
        ????????else?if?((e?=?p.next)?!=?null)?{
        ????????????//?如果是?TreeNode?類型,調(diào)用紅黑樹的查找邏輯定位待刪除節(jié)點
        ????????????if?(p?instanceof?TreeNode)
        ????????????????node?=?((TreeNode)p).getTreeNode(hash,?key);
        ????????????else?{
        ????????????????//?2、遍歷鏈表,找到待刪除節(jié)點
        ????????????????do?{
        ????????????????????if?(e.hash?==?hash?&&?((k?=?e.key)?==?key?||?(key?!=?null?&&?key.equals(k))))?{
        ????????????????????????node?=?e;
        ????????????????????????break;
        ????????????????????}
        ????????????????????p?=?e;
        ????????????????}?while?((e?=?e.next)?!=?null);
        ????????????}
        ????????}
        ????????//?3、刪除節(jié)點,并修復(fù)鏈表或紅黑樹
        ????????if?(node?!=?null?&&?(!matchValue?||?(v?=?node.value)?==?value?||?(value?!=?null?&&?value.equals(v))))?{
        ????????????if?(node?instanceof?TreeNode)
        ????????????????((TreeNode)node).removeTreeNode(this,?tab,?movable);
        ????????????else?if?(node?==?p)
        ????????????????tab[index]?=?node.next;
        ????????????else
        ????????????????p.next?=?node.next;
        ????????????++modCount;
        ????????????--size;
        ????????????afterNodeRemoval(node);
        ????????????return?node;
        ????????}
        ????}
        ????return?null;
        }
        注意:刪除節(jié)點后可能破壞了紅黑樹的平衡性質(zhì),removeTreeNode方法會對紅黑樹進行變色、旋轉(zhuǎn)等操作來保持紅黑樹的平衡結(jié)構(gòu),這部分比較復(fù)雜,感興趣的小伙伴可看下面這篇文章:紅黑樹詳解

        遍歷

        在工作中HashMap的遍歷操作也是非常常用的,也許有很多小伙伴喜歡用for-each來遍歷,但是你知道其中有哪些坑嗎?
        看下面的例子,當我們在遍歷HashMap的時候,若使用remove方法刪除元素時會拋出ConcurrentModificationException異常
        ????Map?map?=?new?HashMap<>();
        ????????map.put("1",?1);
        ????????map.put("2",?2);
        ????????map.put("3",?3);
        ????????for?(String?s?:?map.keySet())?{
        ????????????if?(s.equals("2"))
        ????????????????map.remove("2");
        ????????}
        這就是常說的fail-fast(快速失敗)機制,這個就需要從一個變量說起
        transient?int?modCount;
        在HashMap中有一個名為modCount的變量,它用來表示集合被修改的次數(shù),修改指的是插入元素或刪除元素,可以回去看看上面插入刪除的源碼,在最后都會對modCount進行自增。
        當我們在遍歷HashMap時,每次遍歷下一個元素前都會對modCount進行判斷,若和原來的不一致說明集合結(jié)果被修改過了,然后就會拋出異常,這是Java集合的一個特性,我們這里以keySet為例,看看部分相關(guān)源碼:
        public?Set?keySet()?{
        ????Set?ks?=?keySet;
        ????if?(ks?==?null)?{
        ????????ks?=?new?KeySet();
        ????????keySet?=?ks;
        ????}
        ????return?ks;
        }

        final?class?KeySet?extends?AbstractSet<K>?{
        ????public?final?Iterator?iterator()?????{?return?new?KeyIterator();?}
        ????//?省略部分代碼
        }

        final?class?KeyIterator?extends?HashIterator?implements?Iterator<K>?{
        ????public?final?K?next()?{?return?nextNode().key;?}
        }

        /*HashMap迭代器基類,子類有KeyIterator、ValueIterator等*/
        abstract?class?HashIterator?{
        ????Node?next;????????//下一個節(jié)點
        ????Node?current;?????//當前節(jié)點
        ????int?expectedModCount;??//修改次數(shù)
        ????int?index;?????????????//當前索引
        ????//無參構(gòu)造
        ????HashIterator()?{
        ????????expectedModCount?=?modCount;
        ????????Node[]?t?=?table;
        ????????current?=?next?=?null;
        ????????index?=?0;
        ????????//找到第一個不為空的桶的索引
        ????????if?(t?!=?null?&&?size?>?0)?{
        ????????????do?{}?while?(index?null);
        ????????}
        ????}
        ????//是否有下一個節(jié)點
        ????public?final?boolean?hasNext()?{
        ????????return?next?!=?null;
        ????}
        ????//返回下一個節(jié)點
        ????final?Node?nextNode()?{
        ????????Node[]?t;
        ????????Node?e?=?next;
        ????????if?(modCount?!=?expectedModCount)
        ????????????throw?new?ConcurrentModificationException();//fail-fast
        ????????if?(e?==?null)
        ????????????throw?new?NoSuchElementException();
        ????????//當前的鏈表遍歷完了就開始遍歷下一個鏈表
        ????????if?((next?=?(current?=?e).next)?==?null?&&?(t?=?table)?!=?null)?{
        ????????????do?{}?while?(index?null);
        ????????}
        ????????return?e;
        ????}
        ????//刪除元素
        ????public?final?void?remove()?{
        ????????Node?p?=?current;
        ????????if?(p?==?null)
        ????????????throw?new?IllegalStateException();
        ????????if?(modCount?!=?expectedModCount)
        ????????????throw?new?ConcurrentModificationException();
        ????????current?=?null;
        ????????K?key?=?p.key;
        ????????removeNode(hash(key),?key,?null,?false,?false);//調(diào)用外部的removeNode
        ????????expectedModCount?=?modCount;
        ????}
        }
        相關(guān)代碼如下,可以看到若modCount被修改了則會拋出ConcurrentModificationException異常。
        if?(modCount?!=?expectedModCount)
        ????????????throw?new?ConcurrentModificationException();
        那么如何在遍歷時刪除元素呢?
        我們可以看看迭代器自帶的remove方法,其中最后兩行代碼如下:
        removeNode(hash(key),?key,?null,?false,?false);//調(diào)用外部的removeNode
        expectedModCount?=?modCount;
        意思就是會調(diào)用外部remove方法刪除元素后,把modCount賦值給expectedModCount,這樣的話兩者一致就不會拋出異常了,所以我們應(yīng)該這樣寫:
        Map?map?=?new?HashMap<>();
        ????????map.put("1",?1);
        ????????map.put("2",?2);
        ????????map.put("3",?3);
        ????????Iterator?iterator?=?map.keySet().iterator();
        ????????while?(iterator.hasNext()){
        ????????????if?(iterator.next().equals("2"))
        ????????????????iterator.remove();
        ????????}
        這里還有一個知識點就是在遍歷HashMap時,我們會發(fā)現(xiàn)遍歷的順序和插入的順序不一致,這是為什么?
        在HashIterator源碼里面可以看出,它是先從桶數(shù)組中找到包含鏈表節(jié)點引用的桶。然后對這個桶指向的鏈表進行遍歷。遍歷完成后,再繼續(xù)尋找下一個包含鏈表節(jié)點引用的桶,找到繼續(xù)遍歷。找不到,則結(jié)束遍歷。這就解釋了為什么遍歷和插入的順序不一致,不懂的同學(xué)請看下圖:

        equasl和hashcode

        我在面試中就被問到過HashMap的key有什么限制嗎?相信很多人都知道HashMap的key需要重寫equals和hashcode方法。
        為什么HashMap的key需要重寫equals()和hashcode()方法?
        簡單看個例子,這里以Person為例:
        public?class?Person?{
        ????Integer?id;
        ????String?name;

        ????public?Person(Integer?id,?String?name)?{
        ????????this.id?=?id;
        ????????this.name?=?name;
        ????}

        ????@Override
        ????public?boolean?equals(Object?obj)?{
        ????????if?(obj?==?null)?return?false;
        ????????if?(obj?==?this)?return?true;
        ????????if?(obj?instanceof?Person)?{
        ????????????Person?person?=?(Person)?obj;
        ????????????if?(this.id?==?person.id)
        ????????????????return?true;
        ????????}
        ????????return?false;
        ????}

        ????public?static?void?main(String[]?args)?{
        ????????Person?p1?=?new?Person(1,?"aaa");
        ????????Person?p2?=?new?Person(1,?"bbb");
        ????????HashMap?map?=?new?HashMap<>();
        ????????map.put(p1,?"這是p1");
        ????????System.out.println(map.get(p2));
        ????}
        }
        • 原生的equals方法是使用==來比較對象的
        • 原生的hashCode值是根據(jù)內(nèi)存地址換算出來的一個值
        Person類重寫equals方法來根據(jù)id判斷是否相等,當沒有重寫hashcode方法時,插入p1后便無法用p2取出元素,這是因為p1和p2的哈希值不相等。
        HashMap插入元素時是根據(jù)元素的哈希值來確定存放在數(shù)組中的位置,因此HashMap的key需要重寫equals和hashcode方法。

        總結(jié)

        本文描述了HashMap的實現(xiàn)原理,并結(jié)合源碼做了進一步的分析,其實還有很多相關(guān)的知識點沒有講到,比如HashMap的線程安全問題、1.7和1.8版本之間的區(qū)別....后續(xù)如果有時間的話會繼續(xù)寫文章和大家交流交流,另外博主也針對ConcurrentHashMap寫了一篇文章,想了解的同學(xué)可以去看看一文看懂ConcurrentHashMap,希望本篇文章能幫助到大家,同時也歡迎討論指正,謝謝支持!

        最近熱文

        ? ?思科前員工刪庫跑路,損失達 1600 多萬
        ???如何破解“僅三天可見”的朋友圈?
        ???面試官寫了個雙冒號::問我這是什么語法?Java中有這玩意?
        ???Java反射到底慢在哪?

        最近整理了一份大廠算法刷題指南,包括一些刷題技巧,在知乎上已經(jīng)有上萬贊。同時還整理了一份6000頁面試筆記。關(guān)注下面公眾號,在公眾號內(nèi)回復(fù)「刷題」,即可免費獲?。?span style="letter-spacing: 0.544px;-webkit-tap-highlight-color: rgba(0, 0, 0, 0);font-weight: bolder;">回復(fù)「加群」,可以邀請你加入讀者群!



        明天見(??ω??)??

        瀏覽 48
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        評論
        圖片
        表情
        推薦
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            91午夜福利 | 亚洲AV日韩AV无码裸体尤物 | jizz性欧美19 | 鸡巴视频| 免费的操逼网站 | 三级片国产在线观看 | 成人免费看片 视频\ | 男女生互操| a天堂亚洲一区二区三区在线观看 | 最新日韩欧美色 |