<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          我們一直在用的HashMap,你真正了解嗎?擴容機制又是什么?

          共 4999字,需瀏覽 10分鐘

           ·

          2021-12-11 10:03

          來源:github.com/feigeswjtu

          什么是HashMap?

          HashMap是基于哈希表的Map接口的非同步實現(xiàn)。此實現(xiàn)提供所有可選的映射操作,并允許使用null值和null鍵。此類不保證映射的順序,特別是它不保證該順序恒久不變。

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

          在Java編程語言中,最基本的結(jié)構(gòu)就是兩種,一個是數(shù)組,另外一個是模擬指針(引用),所有的數(shù)據(jù)結(jié)構(gòu)都可以用這兩個基本結(jié)構(gòu)來構(gòu)造的,HashMap也不例外。HashMap實際上是一個“鏈表散列”的數(shù)據(jù)結(jié)構(gòu),即數(shù)組和鏈表的結(jié)合體。

          文字描述永遠要配上圖才能更好的講解數(shù)據(jù)結(jié)構(gòu),HashMap的結(jié)構(gòu)圖如下。

          從上圖中可以看出,HashMap底層就是一個數(shù)組結(jié)構(gòu),數(shù)組中的每一項又是一個鏈表或者紅黑樹。當新建一個HashMap的時候,就會初始化一個數(shù)組。

          下面先通過大概看下HashMap的核心成員。

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

          ????//?默認容量,默認為16,必須是2的冪
          ????static?final?int?DEFAULT_INITIAL_CAPACITY?=?1?<4;

          ????//?最大容量,值是2^30
          ????static?final?int?MAXIMUM_CAPACITY?=?1?<30

          ????//?裝載因子,默認的裝載因子是0.75
          ????static?final?float?DEFAULT_LOAD_FACTOR?=?0.75f;

          ????//?解決沖突的數(shù)據(jù)結(jié)構(gòu)由鏈表轉(zhuǎn)換成樹的閾值,默認為8
          ????static?final?int?TREEIFY_THRESHOLD?=?8;

          ????//?解決沖突的數(shù)據(jù)結(jié)構(gòu)由樹轉(zhuǎn)換成鏈表的閾值,默認為6
          ????static?final?int?UNTREEIFY_THRESHOLD?=?6;

          ????/*?當桶中的bin被樹化時最小的hash表容量。
          ?????*??如果沒有達到這個閾值,即hash表容量小于MIN_TREEIFY_CAPACITY,當桶中bin的數(shù)量太多時會執(zhí)行resize擴容操作。
          ?????*??這個MIN_TREEIFY_CAPACITY的值至少是TREEIFY_THRESHOLD的4倍。
          ?????*/

          ????static?final?int?MIN_TREEIFY_CAPACITY?=?64;

          ????static?class?Node<K,V>?implements?Map.Entry<K,V>?{
          ????????//...
          ????}
          ????//?存儲數(shù)據(jù)的數(shù)組
          ????transient?Node[]?table;

          ????//?遍歷的容器
          ????transient?Set>?entrySet;

          ????//?Map中KEY-VALUE的數(shù)量
          ????transient?int?size;

          ????/**
          ?????*?結(jié)構(gòu)性變更的次數(shù)。
          ?????*?結(jié)構(gòu)性變更是指map的元素數(shù)量的變化,比如rehash操作。
          ?????*?用于HashMap快速失敗操作,比如在遍歷時發(fā)生了結(jié)構(gòu)性變更,就會拋出ConcurrentModificationException。
          ?????*/

          ????transient?int?modCount;

          ????//?下次resize的操作的size值。
          ????int?threshold;

          ????//?負載因子,resize后容量的大小會增加現(xiàn)有size?*?loadFactor
          ????final?float?loadFactor;
          }

          HashMap的初始化

          public?HashMap()?{
          ????this.loadFactor?=?DEFAULT_LOAD_FACTOR;?//?其他值都是默認值
          }

          通過源碼可以看出初始化時并沒有初始化數(shù)組table,那只能在put操作時放入了,為什么要這樣做?估計是避免初始化了HashMap之后不使用反而占用內(nèi)存吧,哈哈哈。

          HashMap的存儲操作

          public?V?put(K?key,?V?value)?{
          ????return?putVal(hash(key),?key,?value,?false,?true);
          }

          下面我們詳細講一下HashMap是如何確定數(shù)組索引的位置、進行put操作的詳細過程以及擴容機制(resize)

          hash計算,確定數(shù)組索引位置

          不管增加、刪除、查找鍵值對,定位到哈希桶數(shù)組的位置都是很關(guān)鍵的第一步。前面說過HashMap的數(shù)據(jù)結(jié)構(gòu)是數(shù)組和鏈表的結(jié)合,所以我們當然希望這個HashMap里面的元素位置盡量分布均勻些,盡量使得每個位置上的元素數(shù)量只有一個,那么當我們用hash算法求得這個位置的時候,馬上就可以知道對應位置的元素就是我們要的,不用遍歷鏈表,大大優(yōu)化了查詢的效率。HashMap定位數(shù)組索引位置,直接決定了hash方法的離散性能。

          看下源碼的實現(xiàn):

          static?final?int?hash(Object?key)?{???//jdk1.8
          ???int?h;
          ???//?h?=?key.hashCode()?為第一步?取hashCode值
          ???//?h?^?(h?>>>?16)??為第二步?高位參與運算
          ???return?(key?==?null)???0?:?(h?=?key.hashCode())?^?(h?>>>?16);
          }

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

          大家都知道上面代碼里的key.hashCode()函數(shù)調(diào)用的是key鍵值類型自帶的哈希函數(shù),返回int型散列值。理論上散列值是一個int型,如果直接拿散列值作為下標訪問HashMap主數(shù)組的話,考慮到2進制32位帶符號的int表值范圍從?2147483648到2147483648。前后加起來大概40億的映射空間。只要哈希函數(shù)映射得比較均勻松散,一般應用是很難出現(xiàn)碰撞的。但問題是一個40億長度的數(shù)組,內(nèi)存是放不下的。你想,HashMap擴容之前的數(shù)組初始大小才16。所以這個散列值是不能直接拿來用的。用之前還要先做對數(shù)組的長度取模運算,得到的余數(shù)才能用來訪問數(shù)組下標。源碼中模運算是在這個indexFor( )函數(shù)里完成。

          bucketIndex?=?indexFor(hash,?table.length);
          //indexFor的代碼也很簡單,就是把散列值和數(shù)組長度做一個"與"操作,
          static?int?indexFor(int?h,?int?length)?{
          ???return?h?&?(length-1);
          }

          順便說一下,這也正好解釋了為什么HashMap的數(shù)組長度要取2的整次冪。因為這樣(數(shù)組長度?1)正好相當于一個“低位掩碼”?!芭c”操作的結(jié)果就是散列值的高位全部歸零,只保留低位值,用來做數(shù)組下標訪問。以初始長度16為例,16?1=15。2進制表示是00000000 0000000000001111。和某散列值做“與”操作如下,結(jié)果就是截取了最低的四位值。

            10100101 11000100 00100101
          & 00000000 00000000 00001111
          ----------------------------------
          00000000 00000000 00000101 //高位全部歸零,只保留末四位

          但這時候問題就來了,這樣就算我的散列值分布再松散,要是只取最后幾位的話,碰撞也會很嚴重。更要命的是如果散列本身做得不好,分布上成等差數(shù)列的漏洞,恰好使最后幾個低位呈現(xiàn)規(guī)律性重復,就無比蛋疼。這時候“擾動函數(shù)”的價值就出來了,說到這大家應該都明白了,看下圖。

          右位移16位,正好是32bit的一半,自己的高半?yún)^(qū)和低半?yún)^(qū)做異或,就是為了混合原始哈希碼的高位和低位,以此來加大低位的隨機性。而且混合后的低位摻雜了高位的部分特征,這樣高位的信息也被變相保留下來。

          putVal方法

          HashMap的put方法執(zhí)行過程可以通過下圖來理解,自己有興趣可以去對比源碼更清楚地研究學習。

          源碼以及解釋如下:

          //?真正的put操作
          final?V?putVal(int?hash,?K?key,?V?value,?boolean?onlyIfAbsent,
          ????????????????boolean?evict)
          ?
          {
          ????Node[]?tab;?Node?p;?int?n,?i;
          ????//?如果table沒有初始化,或者初始化的大小為0,進行resize操作
          ????if?((tab?=?table)?==?null?||?(n?=?tab.length)?==?0)
          ????????n?=?(tab?=?resize()).length;
          ????//?如果hash值對應的桶內(nèi)沒有數(shù)據(jù),直接生成結(jié)點并且把結(jié)點放入桶中
          ????if?((p?=?tab[i?=?(n?-?1)?&?hash])?==?null)
          ????????tab[i]?=?newNode(hash,?key,?value,?null);
          ????//?如果hash值對應的桶內(nèi)有數(shù)據(jù)解決沖突,再放入桶中
          ????else?{
          ????????Node?e;?K?k;
          ????????//判斷put的元素和已經(jīng)存在的元素是相同(hash一致,并且equals返回true)
          ????????if?(p.hash?==?hash?&&
          ????????????((k?=?p.key)?==?key?||?(key?!=?null?&&?key.equals(k))))
          ????????????e?=?p;
          ????????//?put的元素和已經(jīng)存在的元素是不相同(hash一致,并且equals返回true)
          ????????//?如果桶內(nèi)元素的類型是TreeNode,也就是解決hash解決沖突用的樹型結(jié)構(gòu),把元素放入樹種
          ????????else?if?(p?instanceof?TreeNode)
          ????????????e?=?((TreeNode)p).putTreeVal(this,?tab,?hash,?key,?value);
          ????????else?{
          ????????????//?桶內(nèi)元素的類型不是TreeNode,而是鏈表時,把數(shù)據(jù)放入鏈表的最后一個元素上
          ????????????for?(int?binCount?=?0;?;?++binCount)?{
          ????????????????if?((e?=?p.next)?==?null)?{
          ????????????????????p.next?=?newNode(hash,?key,?value,?null);
          ????????????????????//?如果鏈表的長度大于轉(zhuǎn)換為樹的閾值(TREEIFY_THRESHOLD),將存儲元素的數(shù)據(jù)結(jié)構(gòu)變更為樹
          ????????????????????if?(binCount?>=?TREEIFY_THRESHOLD?-?1)?//?-1?for?1st
          ????????????????????????treeifyBin(tab,?hash);
          ????????????????????break;
          ????????????????}
          ????????????????//?如果查已經(jīng)存在key,停止遍歷
          ????????????????if?(e.hash?==?hash?&&
          ????????????????????((k?=?e.key)?==?key?||?(key?!=?null?&&?key.equals(k))))
          ????????????????????break;
          ????????????????p?=?e;
          ????????????}
          ????????}
          ????????//?已經(jīng)存在元素時
          ????????if?(e?!=?null)?{?//?existing?mapping?for?key
          ????????????V?oldValue?=?e.value;
          ????????????if?(!onlyIfAbsent?||?oldValue?==?null)
          ????????????????e.value?=?value;
          ????????????afterNodeAccess(e);
          ????????????return?oldValue;
          ????????}
          ????}
          ????++modCount;
          ????//?如果K-V數(shù)量大于閾值,進行resize操作
          ????if?(++size?>?threshold)
          ????????resize();
          ????afterNodeInsertion(evict);
          ????return?null;
          }

          擴容機制

          HashMap的擴容機制用的很巧妙,以最小的性能來完成擴容。擴容后的容量就變成了變成了之前容量的2倍,初始容量為16,所以經(jīng)過rehash之后,元素的位置要么是在原位置,要么是在原位置再向高下標移動上次容量次數(shù)的位置,也就是說如果上次容量是16,下次擴容后容量變成了16+16,如果一個元素在下標為7的位置,下次擴容時,要不還在7的位置,要不在7+16的位置。

          我們下面來解釋一下Java8的擴容機制是怎么做到的?n為table的長度,圖(a)表示擴容前的key1和key2兩種key確定索引位置的示例,圖(b)表示擴容后key1和key2兩種key確定索引位置的示例,其中hash1是key1對應的哈希與高位運算結(jié)果。

          元素在重新計算hash之后,因為n變?yōu)?倍,那么n-1的mask范圍在高位多1bit(紅色),因此新的index就會發(fā)生這樣的變化:

          因此,我們在擴充HashMap的時候,不需要像JDK1.7的實現(xiàn)那樣重新計算hash,只需要看看原來的hash值新增的那個bit是1還是0就好了,是0的話索引沒變,是1的話索引變成“原索引+oldCap”,可以看看下圖為16擴充為32的resize示意圖:

          而hash值的高位是否為1,只需要和擴容后的長度做與操作就可以了,因為擴容后的長度為2的次冪,所以高位必為1,低位必為0,如10000這種形式,源碼中有e.hash & oldCap來做到這個邏輯。

          這個設計確實非常的巧妙,既省去了重新計算hash值的時間,而且同時,由于新增的1bit是0還是1可以認為是隨機的,因此resize的過程,均勻的把之前的沖突的節(jié)點分散到新的bucket了。這一塊就是JDK1.8新增的優(yōu)化點。有一點注意區(qū)別,JDK1.7中rehash的時候,舊鏈表遷移新鏈表的時候,如果在新表的數(shù)組索引位置相同,則鏈表元素會倒置,但是從上圖可以看出,JDK1.8不會倒置。下面是JDK1.8的resize源碼,寫的很贊,如下:

          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;
          ????????}
          ????????//?沒超過最大值,就擴充為原來的2倍
          ????????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);
          ????}
          ????//?計算新的resize上限
          ????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)?{
          ????????//?把每個bucket都移動到新的buckets中
          ????????for?(int?j?=?0;?j?????????????Node?e;
          ????????????//如果位置上沒有元素,直接為null
          ????????????if?((e?=?oldTab[j])?!=?null)?{
          ????????????????oldTab[j]?=?null;
          ????????????????//如果只有一個元素,新的hash計算后放入新的數(shù)組中
          ????????????????if?(e.next?==?null)
          ????????????????????newTab[e.hash?&?(newCap?-?1)]?=?e;
          ????????????????//如果是樹狀結(jié)構(gòu),使用紅黑樹保存
          ????????????????else?if?(e?instanceof?TreeNode)
          ????????????????????((TreeNode)e).split(this,?newTab,?j,?oldCap);
          ????????????????//如果是鏈表形式
          ????????????????else?{?//?preserve?order
          ????????????????????Node?loHead?=?null,?loTail?=?null;
          ????????????????????Node?hiHead?=?null,?hiTail?=?null;
          ????????????????????Node?next;
          ????????????????????do?{
          ????????????????????????next?=?e.next;
          ????????????????????????//hash碰撞后高位為0,放入低Hash值的鏈表中
          ????????????????????????if?((e.hash?&?oldCap)?==?0)?{
          ????????????????????????????if?(loTail?==?null)
          ????????????????????????????????loHead?=?e;
          ????????????????????????????else
          ????????????????????????????????loTail.next?=?e;
          ????????????????????????????loTail?=?e;
          ????????????????????????}
          ????????????????????????//hash碰撞后高位為1,放入高Hash值的鏈表中
          ????????????????????????else?{
          ????????????????????????????if?(hiTail?==?null)
          ????????????????????????????????hiHead?=?e;
          ????????????????????????????else
          ????????????????????????????????hiTail.next?=?e;
          ????????????????????????????hiTail?=?e;
          ????????????????????????}
          ????????????????????}?while?((e?=?next)?!=?null);
          ????????????????????//?低hash值的鏈表放入數(shù)組的原始位置
          ????????????????????if?(loTail?!=?null)?{
          ????????????????????????loTail.next?=?null;
          ????????????????????????newTab[j]?=?loHead;
          ????????????????????}
          ????????????????????//?高hash值的鏈表放入數(shù)組的原始位置?+?原始容量
          ????????????????????if?(hiTail?!=?null)?{
          ????????????????????????hiTail.next?=?null;
          ????????????????????????newTab[j?+?oldCap]?=?hiHead;
          ????????????????????}
          ????????????????}
          ????????????}
          ????????}
          ????}
          ????return?newTab;
          }



          瀏覽 62
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

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

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  国语对白免费AV | 久久网一区 | 疯狂 自慰爽www看片91 国产999高清无码精品导航 | 欧美黑人干 | 三级片小说视频 |