日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁(yè) > 编程资源 > 编程问答 >内容正文

编程问答

hashmap取值_一万六千字的HashMap深度剖析

發(fā)布時(shí)間:2025/3/19 编程问答 42 豆豆
生活随笔 收集整理的這篇文章主要介紹了 hashmap取值_一万六千字的HashMap深度剖析 小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

概論

  • HashMap 是無(wú)論在工作還是面試中都非常常見常考的數(shù)據(jù)結(jié)構(gòu)。比如 Leetcode 第一題 Two Sum 的某種變種的最優(yōu)解就是需要用到 HashMap 的,高頻考題 LRU Cache 是需要用到 LinkedHashMap 的。HashMap 用起來很簡(jiǎn)單,所以今天我們來從源碼的角度梳理一下Hashmap

  • 隨著JDK(Java Developmet Kit)版本的更新,JDK1.8對(duì)HashMap底層的實(shí)現(xiàn)進(jìn)行了優(yōu)化,例如引入紅黑樹的數(shù)據(jù)結(jié)構(gòu)和擴(kuò)容的優(yōu)化等。

  • HashMap:它根據(jù)鍵的hashCode值存儲(chǔ)數(shù)據(jù),大多數(shù)情況下可以直接定位到它的值,因而具有很快的訪問速度,但遍歷順序卻是不確定的。

  • HashMap最多只允許一條記錄的鍵為null,允許多條記錄的值為null。

  • HashMap非線程安全,即任一時(shí)刻可以有多個(gè)線程同時(shí)寫HashMap,可能會(huì)導(dǎo)致數(shù)據(jù)的不一致。如果需要滿足線程安全,可以用 Collections的synchronizedMap方法使HashMap具有線程安全的能力,或者使用ConcurrentHashMap

HashMap 的繼承關(guān)系

image-20201126201445602

hashmap 的原理

  • 對(duì)于 HashMap 中的每個(gè) key,首先通過 hash function 計(jì)算出一個(gè) hash 值,這個(gè)hash值經(jīng)過取模運(yùn)算就代表了在 buckets 里的編號(hào) buckets 實(shí)際上是用數(shù)組來實(shí)現(xiàn)的,所以把這個(gè)hash值模上數(shù)組的長(zhǎng)度得到它在數(shù)組的 index,就這樣把它放在了數(shù)組里。

  • 如果果不同的元素算出了相同的哈希值,那么這就是哈希碰撞,即多個(gè) key 對(duì)應(yīng)了同一個(gè)桶。這個(gè)時(shí)候就是解決hash沖突的時(shí)候了,展示真正技術(shù)的時(shí)候到了。

  • 隨著插入的元素越來越多,發(fā)生碰撞的概率就越大,某個(gè)桶中的鏈表就會(huì)越來越長(zhǎng),直到達(dá)到一個(gè)閾值,HashMap就受不了了,為了提升性能,會(huì)將超過閾值的鏈表轉(zhuǎn)換形態(tài),轉(zhuǎn)換成紅黑樹的結(jié)構(gòu),這個(gè)閾值是 8 。也就是單個(gè)桶內(nèi)的鏈表節(jié)點(diǎn)數(shù)大于 8 ,就會(huì)將鏈表有可能變身為紅黑樹。

  • 解決Hash沖突的方法

    開放定址法

    這種方法也稱再散列法,其基本思想是:當(dāng)關(guān)鍵字key的哈希地址p=H(key)出現(xiàn)沖突時(shí),以p為基礎(chǔ),產(chǎn)生另一個(gè)哈希地址p1,如果p1仍然沖突,再以p為基礎(chǔ),產(chǎn)生另一個(gè)哈希地址p2,…,直到找出一個(gè)不沖突的哈希地址pi ,將相應(yīng)元素存入其中。這種方法有一個(gè)通用的再散列函數(shù)形式:

    Hi=(H(key)+di)% m i=1,2,…,n

    其中H(key)為哈希函數(shù),m 為表長(zhǎng),di稱為增量序列。增量序列的取值方式不同,相應(yīng)的再散列方式也不同。主要有三種 線性探測(cè)再散列,二次探測(cè)再散列,偽隨機(jī)探測(cè)再散列

    再哈希法

    這種方法是同時(shí)構(gòu)造多個(gè)不同的哈希函數(shù)

    Hi=RH1(key) i=1,2,…,k

    當(dāng)哈希地址Hi=RH1(key)發(fā)生沖突時(shí),再計(jì)算Hi=RH2(key)……,直到?jīng)_突不再產(chǎn)生。這種方法不易產(chǎn)生聚集,但增加了計(jì)算時(shí)間

    鏈地址法

    這種方法的基本思想是將所有哈希地址為i的元素構(gòu)成一個(gè)稱為同義詞鏈的單鏈表,并將單鏈表的頭指針存在哈希表的第i個(gè)單元中,因而查找、插入和刪除主要在同義詞鏈中進(jìn)行。

    鏈地址法適用于經(jīng)常進(jìn)行插入和刪除的情況。

    建立公共溢出區(qū)

    這種方法的基本思想是:將哈希表分為基本表和溢出表兩部分,凡是和基本表發(fā)生沖突的元素,一律填入溢出表。

    hashmap 最終的形態(tài)

    一頓操作猛如虎,搞得原本還是很單純的hashmap 變得這么復(fù)雜,難倒了無(wú)數(shù)英雄好漢,由于鏈表長(zhǎng)度過程,會(huì)導(dǎo)致查詢變慢,所以鏈表慢慢最后演化出了紅黑樹的形態(tài)

    HashMap主體上就是一個(gè)數(shù)組結(jié)構(gòu),每一個(gè)索引位置英文叫做一個(gè) bin,我們這里先管它叫做桶,比如你定義一個(gè)長(zhǎng)度為 8 的 HashMap,那就可以說這是一個(gè)由 8 個(gè)桶組成的數(shù)組。

    當(dāng)我們像數(shù)組中插入數(shù)據(jù)的時(shí)候,大多數(shù)時(shí)候存的都是一個(gè)一個(gè) Node 類型的元素,Node 是 HashMap中定義的靜態(tài)內(nèi)部類

    image-20201127171502527

    Hashmap 的返回值

    很多人以為Hashmap 是沒有返回值的,或者也沒有關(guān)注過Hashmap 的返回值,其實(shí)在你調(diào)用Hashmap的put(key,value) 方法 的時(shí)候,它會(huì)將當(dāng)前key 已經(jīng)有的值返回,然后把你的新值放到對(duì)應(yīng)key 的位置上

    public?class?JavaHashMap?{
    ????public?static?void?main(String[]?args)?{
    ????????HashMap?map?=?new?HashMap();
    ????????String?oldValue?=?map.put("java大數(shù)據(jù)",?"數(shù)據(jù)倉(cāng)庫(kù)");
    ????????System.out.println(oldValue);
    ????????oldValue?=?map.put("java大數(shù)據(jù)",?"實(shí)時(shí)數(shù)倉(cāng)");
    ????????System.out.println(oldValue);
    ????}
    }

    運(yùn)行結(jié)果如下,因?yàn)橐婚_始是沒有值的,所以返回null,后面有值了,put 的時(shí)候就返回了舊的值

    image-20201126202457415

    這里有一個(gè)問題需要注意一下,因?yàn)镸ap的Key,Value 的類型都是引用類型,所以在沒有值的情況下一定返回的是null,而不是0 等初始值。

    HashMap 的關(guān)鍵內(nèi)部元素

    存儲(chǔ)容器 table;

    因?yàn)镠ashMap內(nèi)部是用一個(gè)數(shù)組來保存內(nèi)容的, 它的定義 如下

    transient Node[] table

    如果哈希桶數(shù)組很大,即使較差的Hash算法也會(huì)比較分散,如果哈希桶數(shù)組數(shù)組很小,即使好的Hash算法也會(huì)出現(xiàn)較多碰撞,所以就需要在空間成本和時(shí)間成本之間權(quán)衡,其實(shí)就是在根據(jù)實(shí)際情況確定哈希桶數(shù)組的大小,并在此基礎(chǔ)上設(shè)計(jì)好的hash算法減少Hash碰撞。那么通過什么方式來控制map使得Hash碰撞的概率又小,哈希桶數(shù)組(Node[] table)占用空間又少呢?答案就是好的Hash算法和擴(kuò)容機(jī)制。

    在HashMap中,哈希桶數(shù)組table的長(zhǎng)度length大小必須為2的n次方(一定是合數(shù)),這是一種非常規(guī)的設(shè)計(jì),常規(guī)的設(shè)計(jì)是把桶的大小設(shè)計(jì)為素?cái)?shù)。相對(duì)來說素?cái)?shù)導(dǎo)致沖突的概率要小于合數(shù)

    size 元素個(gè)數(shù)

    size這個(gè)字段其實(shí)很好理解,就是HashMap中實(shí)際存在的鍵值對(duì)數(shù)量。注意和table的長(zhǎng)度length、容納最大鍵值對(duì)數(shù)量threshold的區(qū)別

    Node

    ?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;
    ?????}
    }
    • Node是HashMap的一個(gè)靜態(tài)內(nèi)部類。實(shí)現(xiàn)了Map.Entry接口,本質(zhì)是就是一個(gè)映射(鍵值對(duì)),主要包括 hash、key、value 和 next 的屬性。

    • 我們使用 put 方法像其中加鍵值對(duì)的時(shí)候,就會(huì)轉(zhuǎn)換成 Node 類型。其實(shí)就是newNode(hash, key, value, null);

    TreeNode

    當(dāng)桶內(nèi)鏈表到達(dá) 8 的時(shí)候,會(huì)將鏈表轉(zhuǎn)換成紅黑樹,就是 TreeNode類型,它也是 HashMap中定義的靜態(tài)內(nèi)部類。

    static?final?class?TreeNodeV>?extends?LinkedHashMap.EntryV>?{
    ????TreeNode?parent;??//?red-black?tree?links
    ????TreeNode?left;
    ????TreeNode?right;
    ????TreeNode?prev;????//?needed?to?unlink?next?upon?deletion
    ????boolean?red;
    ????TreeNode(int?hash,?K?key,?V?val,?Node?next)?{super(hash,?key,?val,?next);
    }

    說起TreeNode ,就不得不說其他三個(gè)相關(guān)參數(shù) TREEIFY_THRESHOLD=8 和 UNTREEIFY_THRESHOLD=6 以及 MIN_TREEIFY_CAPACITY=64

    TREEIFY_THRESHOLD=8 指的是鏈表的長(zhǎng)度大于8 的時(shí)候進(jìn)行樹化, UNTREEIFY_THRESHOLD=6 ?說的是當(dāng)元素被刪除鏈表的長(zhǎng)度小于6 的時(shí)候進(jìn)行退化,由紅黑樹退化成鏈表

    MIN_TREEIFY_CAPACITY=64 意思是數(shù)組中元素的個(gè)數(shù)必須大于等于64之后才能進(jìn)行樹化

    modCount

    modCount字段主要用來記錄HashMap內(nèi)部結(jié)構(gòu)發(fā)生變化的次數(shù),主要用于迭代的快速失敗。強(qiáng)調(diào)一點(diǎn),內(nèi)部結(jié)構(gòu)發(fā)生變化指的是結(jié)構(gòu)發(fā)生變化,例如put新鍵值對(duì),但是某個(gè)key對(duì)應(yīng)的value值被覆蓋不屬于結(jié)構(gòu)變化。

    閾值 threshold

    它是加在因子乘以初始值大小,后續(xù)擴(kuò)容的時(shí)候和數(shù)組大小一樣,2倍進(jìn)行擴(kuò)容

    threshold?=?(DEFAULT_LOAD_FACTOR?*?DEFAULT_INITIAL_CAPACITY)

    實(shí)際存儲(chǔ)元素個(gè)數(shù) size

    size 默認(rèn)大小是0 ,它指的是數(shù)組存儲(chǔ)的元素個(gè)數(shù),而不是整個(gè)hashmap 的元素個(gè)數(shù),對(duì)于下面這張圖就是3 而不是11

    transient?int?size;
    image-20201127171502527

    debug 源碼 插入元素的過程

    public?class?JavaHashMap?{
    ????public?static?void?main(String[]?args)?{
    ????????HashMap<String,?String>?map?=?new?HashMap<String,?String>();
    ????????String?oldValue?=?map.put("java大數(shù)據(jù)",?"數(shù)據(jù)倉(cāng)庫(kù)");
    ????}
    }

    調(diào)用put()方法

    這個(gè)方法沒什么好說的,是hashmap 提供給用戶調(diào)用的方法,很簡(jiǎn)單

    調(diào)用 putval()

    Put 方法實(shí)際上調(diào)用的實(shí) ?putval() 方法

    image-20201126204454960

    可以看出在進(jìn)入putval() 方法之間,需要借助hash 方法先計(jì)算出key 的hash 值,然后將key 的hash值和key同時(shí)傳入

    調(diào)用hash() 方法

    image-20201126204634472
    • 這個(gè)key的hashCode()方法得到其hashCode 值(該方法適用于每個(gè)Java對(duì)象),然后再通過Hash算法的后兩步運(yùn)算(高位運(yùn)算和取模運(yùn)算,下文有介紹)來定位該鍵值對(duì)的存儲(chǔ)位置,有時(shí)兩個(gè)key會(huì)定位到相同的位置,表示發(fā)生了Hash碰撞。當(dāng)然Hash算法計(jì)算結(jié)果越分散均勻,Hash碰撞的概率就越小,map的存取效率就會(huì)越高。

    • 在JDK1.8的實(shí)現(xiàn)中,優(yōu)化了高位運(yùn)算的算法,通過hashCode()的高16位異或低16位實(shí)現(xiàn)的:(h = k.hashCode()) ^ (h >>> 16),主要是從速度、功效、質(zhì)量來考慮的,這么做可以在數(shù)組table的length比較小的時(shí)候,也能保證考慮到高低Bit都參與到Hash的計(jì)算中,同時(shí)不會(huì)有太大的開銷。

    進(jìn)入 putval()

    進(jìn)入putval 方法之后,整體數(shù)據(jù)流程如下,下面會(huì)詳細(xì)介紹每一步

    image-20201126204925231final?V?putVal(int?hash,?K?key,?V?value,?boolean?onlyIfAbsent,
    ???????????????boolean?evict)?{
    ????Node[]?tab;?Node?p;?int?n,?i;//?判斷是否需要初始化數(shù)組if?((tab?=?table)?==?null?||?(n?=?tab.length)?==?0)
    ????????n?=?(tab?=?resize()).length;if?((p?=?tab[i?=?(n?-?1)?&?hash])?==?null)//?當(dāng)前位置為空,則直接插入,同時(shí)意味著不走else?最后直接返回null
    ????????tab[i]?=?newNode(hash,?key,?value,?null);else?{
    ????????Node?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)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;
    ????????}
    ????}//?可以看出只有當(dāng)前key?的位置為空的時(shí)候才判斷時(shí)候需要reszie?已經(jīng)返回?null?其他情況下都走了else?的環(huán)節(jié)
    ????++modCount;if?(++size?>?threshold)
    ????????resize();
    ????afterNodeInsertion(evict);return?null;
    }

    判斷數(shù)組是否為空,需不需要調(diào)用resize 方法

    第一次調(diào)用,這里table 是null,所以會(huì)走resize 方法

    image-20201126205708504

    resize 方法本身也是比較復(fù)雜的,因?yàn)檫@里是第一次調(diào)用,所以這里進(jìn)行了簡(jiǎn)化

    ????final?Node[]?resize()?{
    ????????Node[]?oldTab?=?table;int?oldCap?=?(oldTab?==?null)???0?:?oldTab.length;int?oldThr?=?threshold;int?newCap,?newThr?=?0;if?(oldCap?>?0)?{if?(oldCap?>=?MAXIMUM_CAPACITY)?{
    ????????????????threshold?=?Integer.MAX_VALUE;return?oldTab;
    ????????????}else?if?((newCap?=?oldCap?<1)??????????????????????oldCap?>=?DEFAULT_INITIAL_CAPACITY)
    ????????????????newThr?=?oldThr?<1;?//?double?threshold
    ????????}else?if?(oldThr?>?0)?//?initial?capacity?was?placed?in?threshold
    ????????????newCap?=?oldThr;else?{???????????????//??首次初始化?zero?initial?threshold?signifies?using?defaults
    ????????????newCap?=?DEFAULT_INITIAL_CAPACITY;
    ????????????newThr?=?(int)(DEFAULT_LOAD_FACTOR?*?DEFAULT_INITIAL_CAPACITY);
    ????????}if?(newThr?==?0)?{float?ft?=?(float)newCap?*?loadFactor;
    ????????????newThr?=?(newCap?float)MAXIMUM_CAPACITY??
    ??????????????????????(int)ft?:?Integer.MAX_VALUE);
    ????????}
    ????????threshold?=?newThr;@SuppressWarnings({"rawtypes","unchecked"})
    ????????Node[]?newTab?=?(Node[])new?Node[newCap];
    ????????table?=?newTab;if?(oldTab?!=?null)?{//?因?yàn)?oldTab?為null?所以不會(huì)進(jìn)來這個(gè)if?判斷,所以將這里的代碼省略了
    ????????}return?newTab;
    ????}
    table 為空首次初始化

    如果是的話,初始化數(shù)組大小和threashold

    newCap?=?DEFAULT_INITIAL_CAPACITY;
    newThr?=?(int)(DEFAULT_LOAD_FACTOR?*?DEFAULT_INITIAL_CAPACITY);

    初始化之后,將新創(chuàng)建的數(shù)組返回,在返回之前完成了對(duì)變量table 的賦值

    image-20201126211551514
    table 不為空 不是首次初始化

    如果不是的話就用當(dāng)前數(shù)組的信息初始化新數(shù)組的大小

    image-20201126211919741

    最后完成table 的初始化,返回table ,這里其實(shí)還有數(shù)據(jù)遷移,但是為了保證文章的結(jié)構(gòu),所以將resize 方法的詳細(xì)講解單獨(dú)提了出來

    table?=?newTab;

    判斷當(dāng)前位置是否有元素

    1 沒有 直接放入當(dāng)前位置
    2 有 將當(dāng)前節(jié)點(diǎn)記做p

    當(dāng)前節(jié)點(diǎn)記做p 然后進(jìn)入else 循環(huán)

    else?{
    ????Node?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)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;
    ????}
    ?}
    判斷直接覆蓋(判斷是否是同一個(gè)key)

    判斷新的key 和老的key 是否相同,這里同時(shí)要求了hash 值和 實(shí)際的值是相等的情況下然后直接完成了e=p 的賦值,其實(shí)也就是完成了替換,因?yàn)閗ey 是相同的。

    如果不是同一個(gè)key 的話這里就要將當(dāng)前元素插入鏈表或者紅黑樹了,因?yàn)槭遣煌膋ey 了

    判斷插入紅黑樹

    如果當(dāng)前元素是一個(gè) TreeNode 則將當(dāng)前元素放入紅黑樹,然后

    image-20201126220247642
    判斷插入鏈表
    • 如果不是同一key并且當(dāng)前元素類型不是TreeNode 則將當(dāng)前元素插入鏈表(因?yàn)閗ey對(duì)應(yīng)的位置已經(jīng)有元素了,其實(shí)可以認(rèn)為是鏈表的頭元素)

    • 可以看出采用的是尾插法,循環(huán)過程中當(dāng)下一個(gè)節(jié)點(diǎn)是null的時(shí)候則進(jìn)行插入,插入完畢之后判斷是否需要樹化

    JDK 1.7 之前使用頭插法、JDK 1.8 使用尾插法

    • 其實(shí)主要是根據(jù)(e=p.next)==null 進(jìn)行判斷進(jìn)入哪一個(gè)if ,因?yàn)槊總€(gè) if 都含有break 語(yǔ)句,所以只能進(jìn)入一個(gè) 然后就退出循環(huán)了

      image-20201126220940075?if?((e?=?p.next)?==?null)?{
      ?????p.next?=?newNode(hash,?key,?value,?null);
      ?????if?(binCount?>=?TREEIFY_THRESHOLD?-?1)?//?-1?for?1st
      ?????????treeifyBin(tab,?hash);
      ?????break;
      ?}

      1、這段代碼也是上圖中的第一個(gè)if這段代碼的意思就是在遍歷鏈表的過程中,一直都沒有遇到和待插入key 相同的key(第二個(gè)if) 然后當(dāng)前要插入的元素插入到了鏈表的尾部(當(dāng)前if 語(yǔ)句)

    第二個(gè)if 的意思 如果有發(fā)生key沖突則停止 后續(xù)這個(gè)節(jié)點(diǎn)會(huì)被相同的key覆蓋

    2、插入之后判斷判斷局部變量binCount 時(shí)候大于7(TREEIFY_THRESHOLD-1),這里需要注意的是binCount 是從0開始的,所以實(shí)際的意思是判斷鏈表的長(zhǎng)度在插入新元素之前是否大于等于8,如果是的話則進(jìn)行樹化

    3、并且這個(gè)時(shí)候變量e 的值是null ,因?yàn)槭遣迦氲芥湵淼奈膊康?#xff0c;所以這個(gè)時(shí)候key 是沒有對(duì)應(yīng)的oldValue 的,所以e是null 在最后面的判斷返回中,也返回的是null

    4、關(guān)于樹化,首先這是發(fā)生在插入鏈表的時(shí)刻,并且是插入鏈表尾部的時(shí)候,因?yàn)榕袛噙^程是在第一個(gè)if 中,為了保證文章的結(jié)構(gòu)關(guān)于樹化放在下面講

    if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; // 這個(gè)賦值很有意思,它完成了你可以使用for 循環(huán)完成鏈表遍歷的核心功能 p = e;

    1、這一段代碼的意思是在遍歷的過程中(e=p.next)!=null 的的時(shí)候,也就是在循環(huán)鏈表的過程中,判斷是否有和當(dāng)前key 相等的key,相等的話e 就是要覆蓋的元素,如果不相等的話就繼續(xù)循環(huán),知道找到這樣的e 或者是將鏈表循環(huán)結(jié)束,然后將元素插入到鏈表的尾部(第一個(gè)if)

    2、因?yàn)槭钱?dāng)key 存在的時(shí)候則跳出循環(huán),所以鏈表的長(zhǎng)度沒有發(fā)生變化,所以這里沒有判斷是否需要樹化

    最后 返回oldValue 完成新值替換
    if?(e?!=?null)?{?//?existing?mapping?for?key
    ????V?oldValue?=?e.value;
    ????if?(!onlyIfAbsent?||?oldValue?==?null)
    ????????e.value?=?value;
    ????afterNodeAccess(e);
    ????return?oldValue;
    }

    這個(gè)時(shí)候e 就指向原來p 的位置了,因?yàn)閑=p, 然后用新的value 覆蓋掉了oldValue 完成了插入,最后將 oldValue 返回。

    最后 判斷是否需要擴(kuò)容 返回null 值

    其實(shí)能走到這一步,是那就說明放入元素的時(shí)候,key 對(duì)應(yīng)的位置是沒有元素的,所以相當(dāng)于數(shù)組中添加了一個(gè)新的元素,所以這里有判斷是否需要resize 和返回空值。

    ?++modCount;
    ?if?(++size?>?threshold)
    ?????resize();
    ?afterNodeInsertion(evict);
    ?return?null;

    單獨(dú)講解resize 方法

    首選需要記住resize 方法是會(huì)返回?cái)U(kuò)容后的數(shù)組的

    第一部分初始化新數(shù)組

    這一部分不論是不是首次調(diào)用resize 方法,都會(huì)有的,但是數(shù)據(jù)遷移部分在首次調(diào)用的時(shí)候是沒有的

    Node[]?oldTab?=?table;int?oldCap?=?(oldTab?==?null)???0?:?oldTab.length;int?oldThr?=?threshold;int?newCap,?newThr?=?0;//?判斷是oldCap?是否大于0?因?yàn)榭赡苁鞘状蝦esize,如果不是的話?oldCapif?(oldCap?>?0)?{//?到達(dá)擴(kuò)容上限if?(oldCap?>=?MAXIMUM_CAPACITY)?{
    ????????threshold?=?Integer.MAX_VALUE;return?oldTab;
    ????}//?這里是正常的擴(kuò)容else?if?((newCap?=?oldCap?<1)??????????????oldCap?>=?DEFAULT_INITIAL_CAPACITY)
    ????????newThr?=?oldThr?<1;?//?double?threshold
    }else?if?(oldThr?>?0)?//?initial?capacity?was?placed?in?threshold
    ????newCap?=?oldThr;//第一次調(diào)用resize?方法,然后使用默認(rèn)值進(jìn)行初始化else?{//?zero?initial?threshold?signifies?using?defaults
    ????newCap?=?DEFAULT_INITIAL_CAPACITY;
    ????newThr?=?(int)(DEFAULT_LOAD_FACTOR?*?DEFAULT_INITIAL_CAPACITY);
    }if?(newThr?==?0)?{float?ft?=?(float)newCap?*?loadFactor;
    ????newThr?=?(newCap?float)MAXIMUM_CAPACITY??
    ??????????????(int)ft?:?Integer.MAX_VALUE);
    }//?創(chuàng)建新的數(shù)組,下面
    threshold?=?newThr;@SuppressWarnings({"rawtypes","unchecked"})
    Node[]?newTab?=?(Node[])new?Node[newCap];
    table?=?newTab;
  • 如果數(shù)組的大小 大于等于MAXIMUM_CAPACITY之后,則 threshold = Integer.MAX_VALUE; 然后不擴(kuò)容直接返回當(dāng)前數(shù)組,所以可以看出hashmap 的擴(kuò)容上限就是MAXIMUM_CAPACITY(230)

  • 如果數(shù)組的大小 在擴(kuò)容之后小于MAXIMUM_CAPACITY 并且原始大小大于DEFAULT_INITIAL_CAPACITY(16) 則進(jìn)行擴(kuò)容(DEFAULT_INITIAL_CAPACITY 的大小限制是為了防止該方法的調(diào)用是在樹化方法里調(diào)用的,這個(gè)時(shí)候數(shù)組大大小可能小于DEFAULT_INITIAL_CAPACITY)

  • 新的數(shù)組創(chuàng)建好之后,就可以根據(jù)老的數(shù)組是否有值決定是否進(jìn)行數(shù)據(jù)遷移

  • 第二部分?jǐn)?shù)據(jù)遷移

    oldTab 也就是老的數(shù)組不為空的時(shí)候進(jìn)行遷移

    ?if?(oldTab?!=?null)?{
    ?????????????//?遍歷oldTable,拿到每一個(gè)元素準(zhǔn)備放入大新的數(shù)組中去
    ??????for?(int?j?=?0;?j???????????Node?e;if?((e?=?oldTab[j])?!=?null)?{
    ??????????????oldTab[j]?=?null;//?當(dāng)前元素只是單個(gè)元素,不是鏈表if?(e.next?==?null)//?重新計(jì)算每個(gè)元素在數(shù)組中的位置
    ??????????????????newTab[e.hash?&?(newCap?-?1)]?=?e;//?判斷當(dāng)前元素是否是樹???else?if?(e?instanceof?TreeNode)
    ??????????????????((TreeNode)e).split(this,?newTab,?j,?oldCap);//?當(dāng)前元素是鏈表,則遍歷鏈表????else?{?//?preserve?order
    ??????????????????Node?loHead?=?null,?loTail?=?null;
    ??????????????????Node?hiHead?=?null,?hiTail?=?null;
    ??????????????????Node?next;do?{
    ??????????????????????next?=?e.next;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;
    ??????????????????}
    ??????????????}
    ??????????}
    ??????}
    ??}
    • 判斷當(dāng)前元素的next 是否為空,是則直接放入,其實(shí)就是只有一個(gè)元素,說明這是一個(gè)最正常的節(jié)點(diǎn),不是桶內(nèi)鏈表,也不是紅黑樹,這樣的節(jié)點(diǎn)會(huì)重新計(jì)算索引位置,然后插入。

    • 是的話,判斷是不是TreeNode,不是的話則直接遍歷鏈表進(jìn)行拷貝,保證鏈表的順序不變。

    • 是的話則調(diào)用 TreeNode.split() ?方法,如果是一顆紅黑樹,則使用 split方法處理,原理就是將紅黑樹拆分成兩個(gè) TreeNode 鏈表,然后判斷每個(gè)鏈表的長(zhǎng)度是否小于等于 6,如果是就將 TreeNode 轉(zhuǎn)換成桶內(nèi)鏈表,否則再轉(zhuǎn)換成紅黑樹。

    • 完成數(shù)據(jù)的拷貝,返回新的數(shù)組

    第三部分 返回新的數(shù)組

    ?return?newTab;

    只要沒有到達(dá)擴(kuò)容上限,這一部分是肯定會(huì)走的,至于走不走數(shù)據(jù)遷移,需要潘丹是不是首次resize()

    單獨(dú)講解樹化treeifyBin方法

    ?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;
    ?}
    • 首先判斷是符滿足鏈表長(zhǎng)度大于8(binCount 是否大于等于7) ,需要注意的是插入到鏈表的尾部導(dǎo)致鏈表的長(zhǎng)度發(fā)生了變化的情況下,才判斷是否需要樹化

    • 然后進(jìn)入treeifyBin 方法中,進(jìn)入樹化方法之后又判斷了,Hashmap 的大小是否大于64,如果不是的話,只是調(diào)用了resize 方法,讓數(shù)組擴(kuò)容,而不是樹化

    final?void?treeifyBin(Node[]?tab,?int?hash)?{
    ????int?n,?index;?Node?e;if?(tab?==?null?||?(n?=?tab.length)?????????resize();else?if?((e?=?tab[index?=?(n?-?1)?&?hash])?!=?null)?{
    ????????TreeNode?hd?=?null,?tl?=?null;do?{
    ????????????TreeNode?p?=?replacementTreeNode(e,?null);if?(tl?==?null)
    ????????????????hd?=?p;else?{
    ????????????????p.prev?=?tl;
    ????????????????tl.next?=?p;
    ????????????}
    ????????????tl?=?p;
    ????????}?while?((e?=?e.next)?!=?null);if?((tab[index]?=?hd)?!=?null)
    ????????????hd.treeify(tab);
    ????}
    }

    獲取元素的過程

    public?V?get(Object?key)?{
    ????Node?e;return?(e?=?getNode(hash(key),?key))?==?null???null?:?e.value;
    }/**
    ?*?Implements?Map.get?and?related?methods.
    ?*
    ?*?@param?hash?hash?for?key
    ?*?@param?key?the?key
    ?*?@return?the?node,?or?null?if?none
    ?*/final?Node?getNode(int?hash,?Object?key)?{
    ????Node[]?tab;?Node?first,?e;?int?n;?K?k;if?((tab?=?table)?!=?null?&&?(n?=?tab.length)?>?0?&&
    ????????(first?=?tab[(n?-?1)?&?hash])?!=?null)?{if?(first.hash?==?hash?&&?//?always?check?first?node
    ????????????((k?=?first.key)?==?key?||?(key?!=?null?&&?key.equals(k))))return?first;if?((e?=?first.next)?!=?null)?{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;
    }image-20201127190819337

    總結(jié)

    resize 方法總結(jié)

    resize(擴(kuò)容) 的上限

    resize 不是無(wú)限的,當(dāng)?shù)竭_(dá)resize 的上限,也就是230 之后,不再擴(kuò)容

    resize 方法只有三種情況下調(diào)用

    -?第一種?是在**首次插入元素的時(shí)候完成數(shù)組的初始化**
    -?第二種?是在元素插入**完成后**判斷是否需要數(shù)組擴(kuò)容,如果是的話則調(diào)用
    -?第三種?是在元素插入鏈表尾部之后,進(jìn)入樹化方法之后,如果不樹化則進(jìn)行resize?

    resize 的返回值

    • 第一種情況下 返回老的數(shù)組也就是沒有resize 因?yàn)橐呀?jīng)達(dá)到resize 的上限了

    • 第二種情況下 返回一個(gè)空的數(shù)組 也就是第一次調(diào)用resize方法

    • 第三章情況下 返回一個(gè)擴(kuò)容后的數(shù)組 完成了數(shù)據(jù)遷移后的數(shù)組

    key 的判斷

    • 第一次判斷是當(dāng)前位置有元素的時(shí)候,如果兩個(gè)key 相等則準(zhǔn)備覆蓋值

    • 第二次判斷是遍歷鏈表的時(shí)候,決定能否覆蓋鏈表中間key 相等的值而不是鏈表的尾部

    樹化

    • 樹化是發(fā)生在元素插入鏈表之后,并且這里是插入到鏈表的尾部導(dǎo)致鏈表的長(zhǎng)度發(fā)生了變化的情況下(也就是走的for循環(huán)里的第一個(gè)if 語(yǔ)句),而不是替換了鏈表里面的某一元素(也就是走的for循環(huán)里的第二個(gè)if 語(yǔ)句)

      image-20201127114314435final?void?treeifyBin(Node[]?tab,?int?hash)?{
      ??int?n,?index;?Node?e;if?(tab?==?null?||?(n?=?tab.length)???????resize();else?if?((e?=?tab[index?=?(n?-?1)?&?hash])?!=?null)?{
      ??????TreeNode?hd?=?null,?tl?=?null;do?{
      ??????????TreeNode?p?=?replacementTreeNode(e,?null);if?(tl?==?null)
      ??????????????hd?=?p;else?{
      ??????????????p.prev?=?tl;
      ??????????????tl.next?=?p;
      ??????????}
      ??????????tl?=?p;
      ??????}?while?((e?=?e.next)?!=?null);if?((tab[index]?=?hd)?!=?null)
      ??????????hd.treeify(tab);
      ??}
      }

      其實(shí)這代碼上面有一段注釋的,這里也帖一下,在table 太小的情況下,使用resize 否則替換指的位置鏈表上的全部Nodes(其實(shí)就是替換成紅黑樹)

      /**
      *?Replaces?all?linked?nodes?in?bin?at?index?for?given?hash?unless
      *?table?is?too?small,?in?which?case?resizes?instead.
      */

      其實(shí)這里有一個(gè)隱含的意義,就是數(shù)組不大的時(shí)候,希望通過resize 的方法降低hash 沖突的概率,從而避免鏈表過長(zhǎng)降低查詢時(shí)間,但是當(dāng)數(shù)組比較大的時(shí)候reszie 成本太高,則通過將鏈表轉(zhuǎn)化成紅黑樹來降低查詢時(shí)間

    for 循環(huán)遍歷鏈表而不是while

    這是源代碼里面的一段,上面也解釋過了,這里使用for 循環(huán)遍歷鏈表,利用for 循環(huán)的index 進(jìn)行計(jì)數(shù),這里進(jìn)行了刪減

    for?(int?binCount?=?0;?;?++binCount)?{
    ????if?((e?=?p.next)?==?null)?{
    ????????????doSomething();
    ????????break;

    ????p?=?e;
    }

    番外篇

    hash 方法的實(shí)現(xiàn)方式

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

    JDK 1.8 中,是通過 hashCode() 的高 16 位異或低 16 位實(shí)現(xiàn)的:(h = k.hashCode()) ^ (h >>> 16),主要是從速度,功效和質(zhì)量來考慮的,減少系統(tǒng)的開銷,也不會(huì)造成因?yàn)楦呶粵]有參與下標(biāo)的計(jì)算,從而引起的碰撞

    為什么要用異或運(yùn)算符? 保證了對(duì)象的 hashCode 的 32 位值只要有一位發(fā)生改變,整個(gè) hash() 返回值就會(huì)改變。盡可能的減少碰撞。

    鏈表法導(dǎo)致的鏈表過深問題為什么不用二叉查找樹代替

    之所以選擇紅黑樹是為了解決二叉查找樹的缺陷,二叉查找樹在特殊情況下會(huì)變成一條線性結(jié)構(gòu)(這就跟原來使用鏈表結(jié)構(gòu)一樣了,造成很深的問題),遍歷查找會(huì)非常慢。

    而紅黑樹在插入新數(shù)據(jù)后可能需要通過左旋,右旋、變色這些操作來保持平衡,引入紅黑樹就是為了查找數(shù)據(jù)快,解決鏈表查詢深度的問題,我們知道紅黑樹屬于平衡二叉樹,但是為了保持“平衡”是需要付出代價(jià)的,但是該代價(jià)所損耗的資源要比遍歷線性鏈表要少,所以當(dāng)長(zhǎng)度大于8的時(shí)候,會(huì)使用紅黑樹,如果鏈表長(zhǎng)度很短的話,根本不需要引入紅黑樹,引入反而會(huì)慢

    jdk8中對(duì)HashMap做了哪些改變

    在java 1.8中,如果鏈表的長(zhǎng)度超過了8,那么鏈表將轉(zhuǎn)換為紅黑樹。(桶的數(shù)量必須大于64,小于64的時(shí)候只會(huì)擴(kuò)容)

    發(fā)生hash碰撞時(shí),java 1.7 會(huì)在鏈表的頭部插入,而java 1.8會(huì)在鏈表的尾部插入

    在java 1.8中,Entry被Node替代(換了一個(gè)馬甲)

    Hashmap 的容量大小為什么要求是2n

    這里首選要說明一個(gè)前提,那就是元素在數(shù)組中的位置的計(jì)算方式是 tab[i = (n - 1) & hash] 也就是通過對(duì)數(shù)組大小求模得到的,因?yàn)槲覀冎纇ash 的計(jì)算方式是 ?hashCode() 的高 16 位異或低 16 位實(shí)現(xiàn)的,32 位值只要有一位發(fā)生改變,整個(gè) hash() 返回值就會(huì)改變,也就是說我們的hash 值發(fā)生沖突的概率是比較小的,也就是說hash 值是比較隨機(jī)的

    所以更多的沖突是發(fā)生在取模的時(shí)候,所以這個(gè)時(shí)候只要保證了我們的取模運(yùn)算 (n - 1) & hash,盡量能保證hash 值的特性也就是隨機(jī)性。因?yàn)槲覀冎琅c運(yùn)算的特點(diǎn)是,兩位同時(shí)為“1”,結(jié)果才為“1”,否則為0

    所以這個(gè)時(shí)候我們只要 (n - 1) 讓的二進(jìn)制表示都是一串1,例如"011111" 就可以了,因?yàn)榘参慌c1 結(jié)果是不變的,也就是可以延續(xù)hash 值的散列性

    其實(shí)到這里就差不多了,然后我們看2n 的表示特點(diǎn),然后就知道為什么要就hashmap 的大小是 2n了, 2n次方的二進(jìn)制表示大家肯定都很清楚,2的6次方,就是從右向左 6 個(gè) 0,然后第 7 位是 1

    image-20201127184124095

    其實(shí)這下我們就知道為什么了,因?yàn)橹挥袛?shù)組的長(zhǎng)度是2的次方了,n-1 的二進(jìn)制才能盡可能多的是1

    總結(jié)

    以上是生活随笔為你收集整理的hashmap取值_一万六千字的HashMap深度剖析的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。

    主站蜘蛛池模板: 国产精品视频久久久 | 免费se99se | 亚洲av无码乱码在线观看富二代 | 黑白配av | 免费黄色网址视频 | av夜色 | 中国一级特黄毛片大片 | 国产欧美一区在线观看 | 日本一道本| 日本韩国免费观看 | 色偷偷免费费视频在线 | 免费观看成人鲁鲁鲁鲁鲁视频 | 日韩特级片 | 亚洲视频不卡 | 朝鲜美女黑毛bbw | 日本在线免费看 | 国产中出| 一区二区三区不卡视频在线观看 | 日本精品少妇 | 97香蕉碰碰人妻国产欧美 | 日本黄色一区 | 欧美精品做受xxx性少妇 | 国产视频综合 | 欧美精品一区在线 | 国产污污视频在线观看 | 国产真实生活伦对白 | 91在线资源 | 日韩一区二区三区网站 | av免费网站观看 | 日韩在线免费看 | 国产精品破处 | 亚色图 | 日韩欧美亚洲一区二区三区 | 国产情侣一区二区三区 | 米奇av | 日韩亚洲一区二区 | 熟女一区二区三区视频 | 日韩av大片 | aa亚洲| 一二区视频 | 日韩欧美不卡 | 久草国产在线视频 | 精品一区二区三区精华液 | 亚洲乱码一区二区 | 国产特级毛片aaaaaa | 在线视频日韩精品 | 精品在线视频观看 | 色婷婷免费视频 | 少妇又紧又深又湿又爽视频 | 樱花电影最新免费观看国语版 | 视频一区二区视频 | 午夜成人鲁丝片午夜精品 | 午夜香蕉视频 | 在线日韩三级 | 天天看夜夜 | 青青视频在线播放 | 国产麻豆一精品一男同 | 999久久久久| 久久依人 | 最新中文字幕第一页 | 日本国产精品 | 麻豆av免费在线观看 | 欧美精品在线一区 | 国产精品1000 | 毛片日本 | 三级影片在线免费观看 | 中文字幕日韩在线视频 | 日韩欧美精品久久 | 嫩草www | 韩日激情视频 | 国产成人精品在线播放 | 黄色91免费版 | 国产日韩精品在线 | 中文字幕Av日韩精品 | 张津瑜国内精品www在线 | 黄色视屏软件 | 夜夜操网 | 欧美乱妇15p | 老司机午夜免费精品视频 | 爱爱免费视频网站 | 欧美精品国产一区 | 国产成人三级在线播放 | 国产成人免费视频网站 | 久久精品国产亚洲av麻豆图片 | 91九色网站 | 久久久久久综合网 | 影音先锋 日韩 | av一道本 | 国产男女猛烈无遮挡a片漫画 | 欧美精品在线一区二区 | 亚洲久久一区二区 | 午夜a级片 | 插吧插吧网 | 9·1·黄·色·视·频 | 在线精品视频一区 | 午夜福利啪啪片 | 婷婷色在线观看 | 国产毛片毛片毛片毛片毛片毛片 | 黄页av |