溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點(diǎn)擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

JDK源碼分析(5)之 HashMap 相關(guān)

發(fā)布時(shí)間:2020-08-01 22:50:26 來源:網(wǎng)絡(luò) 閱讀:255 作者:沙漏半杯 欄目:編程語言

HashMap作為我們最常用的數(shù)據(jù)類型,當(dāng)然有必要了解一下他內(nèi)部是實(shí)現(xiàn)細(xì)節(jié)。相比于 JDK7 在JDK8 中引入了紅黑樹以及hash計(jì)算等方面的優(yōu)化,使得 JDK8 中的HashMap效率要高于以往的所有版本,本文會詳細(xì)介紹相關(guān)的優(yōu)化,但是主要還是寫 JDK8 的源碼。

一、整體結(jié)構(gòu)

1. 類定義

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

JDK源碼分析(5)之 HashMap 相關(guān)

可以看到HashMap是完全基于Map接口實(shí)現(xiàn)的,其中AbstractMapMap接口的骨架實(shí)現(xiàn),提供了Map接口的最小實(shí)現(xiàn)。
HashMap看名字也能猜到,他是基于哈希表實(shí)現(xiàn)的(數(shù)組+鏈表+紅黑樹):

JDK源碼分析(5)之 HashMap 相關(guān)

2. 構(gòu)造函數(shù)和成員變量

public?HashMap(int?initialCapacity)public?HashMap()public?HashMap(Map<??extends?K,???extends?V>?m)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);
}

HashMap一共有四個(gè)構(gòu)造函數(shù),其主要作用就是初始化loadFactorthreshold兩個(gè)參數(shù):

  • threshold:擴(kuò)容的閾值,當(dāng)放入的鍵值對大于這個(gè)閾值的時(shí)候,就會發(fā)生擴(kuò)容;

  • loadFactor:負(fù)載系數(shù),用于控制閾值的大小,即threshold = table.length * loadFactor;默認(rèn)情況下負(fù)載系數(shù)等于0.75,當(dāng)它值越大時(shí):哈希桶空余的位置越少,空間利用率越高,同時(shí)哈希沖突也就越嚴(yán)重,效率也就越低;相反它值越小時(shí):空間利用率越低,效率越高;而0.75是對于空間和效率的一個(gè)平衡,通常情況下不建議修改;

但是對于上面構(gòu)造函數(shù)當(dāng)中this.threshold = tableSizeFor(initialCapacity);,這里的閾值并沒有乘以負(fù)載系數(shù),是因?yàn)樵跇?gòu)造函數(shù)當(dāng)中哈希桶table[]還沒有初始化,在往里put數(shù)據(jù)的時(shí)候才會初始化,而tableSizeFor是為了得到大于等于initialCapacity的最小的2的冪;

transient?Node<K,V>[]?table;????????????//?哈希桶transient?Set<Map.Entry<K,V>>?entrySet;?//?映射關(guān)系Set視圖transient?int?size;?????????????????????//?鍵值對的數(shù)量transient?int?modCount;?????????????????//?結(jié)構(gòu)修改次數(shù),用于實(shí)現(xiàn)fail-fast機(jī)制

哈希桶的結(jié)構(gòu)如下:

static?class?Node<K,V>?implements?Map.Entry<K,V>?{??final?int?hash;???????//?用于尋址,避免重復(fù)計(jì)算
??final?K?key;
??V?value;
??Node<K,V>?next;
??...??public?final?int?hashCode()?{????return?Objects.hashCode(key)?^?Objects.hashCode(value);
??}
}

其中Node<K,V> next還有一個(gè)TreeNode子類用于實(shí)現(xiàn)紅黑樹,需要注意的是這里的hashCode()所計(jì)算的hash值只用于在遍歷的時(shí)候獲取hash值,并非尋址所用hash;

二、Hash表

既然是Hash表,那么最重要的肯定是尋址了,在HashMap中采用的是除留余數(shù)法,即table[hash % length],但是在現(xiàn)代CPU中求余是最慢的操作,所以人們想到一種巧妙的方法來優(yōu)化它,即length為2的指數(shù)冪時(shí),hash % length = hash & (length-1),所以在構(gòu)造函數(shù)中需要使用tableSizeFor(int cap)來調(diào)整初始容量;

/**
?*?Returns?a?power?of?two?size?for?the?given?target?capacity.
?*/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;
}

首先這里要明確:

  • 2的冪的二進(jìn)制是,1后面全是0

  • 有效位都是1的二進(jìn)制加1,就可以得到2的冪

以33為例,如圖:

JDK源碼分析(5)之 HashMap 相關(guān)

因?yàn)閕nt是4個(gè)字節(jié)32位,所以最多只需要將高位的16位與低位的16位做或運(yùn)算就可以得到2的冪,而int n = cap - 1;是為了避免cap本身就是2的冪的情況;這個(gè)算是真是厲害,看了很久才看明白,實(shí)在汗顏。

計(jì)算 hash

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

這里重新計(jì)算hash是因?yàn)樵?code >hash & (length-1)計(jì)算下標(biāo)的時(shí)候,實(shí)際只有hash的低位參與的運(yùn)算容易產(chǎn)生hash沖突,所以用異或是高位的16位也參與運(yùn)算,以減小hash沖突,要理解這里首先要明白,

  • & 操作之后只會保留下都是1的有效位

  • length-1(2的n次方-1)實(shí)際上就是n和1

  • & 操作之后hash所保留下來的也只有低位的n個(gè)有效位,所以實(shí)際只有hash的低位參與了運(yùn)算

具體如圖所示:

JDK源碼分析(5)之 HashMap 相關(guān)

三、重要方法講解

對于Map而言最重要的當(dāng)然是GetPut等操作了,所以下面將介紹與之相關(guān)的操作;

1. put方法

public?V?put(K?key,?V?value)?{??return?putVal(hash(key),?key,?value,?false,?true);
}/**
?*?Implements?Map.put?and?related?methods?*?*?@param?hash?hash?for?key
?*?@param?key?the?key
?*?@param?value?the?value?to?put
?*?@param?onlyIfAbsent?if?true,?don't?change?existing?value
?*?@param?evict?if?false,?the?table?is?in?creation?mode.
?*?@return?previous?value,?or?null?if?none
?*/final?V?putVal(int?hash,?K?key,?V?value,?boolean?onlyIfAbsent,?boolean?evict)?{
??Node<K,V>[]?tab;?Node<K,V>?p;?int?n,?i;??//?如果沒有初始化哈希桶,就使用resize初始化
??if?((tab?=?table)?==?null?||?(n?=?tab.length)?==?0)
????n?=?(tab?=?resize()).length;??//?如果hash對應(yīng)的哈希槽是空的,就直接放入
??if?((p?=?tab[i?=?(n?-?1)?&?hash])?==?null)
????tab[i]?=?newNode(hash,?key,?value,?null);??else?{
????Node<K,V>?e;?K?k;????//?如果已經(jīng)存在key,就替換舊值
????if?(p.hash?==?hash?&&?((k?=?p.key)?==?key?||?(key?!=?null?&&?key.equals(k))))
??????e?=?p;????//?如果已經(jīng)是樹節(jié)點(diǎn),就用putTreeVal遍歷樹賦值
????else?if?(p?instanceof?TreeNode)
??????e?=?((TreeNode<K,V>)p).putTreeVal(this,?tab,?hash,?key,?value);????else?{??????//?遍歷鏈表
??????for?(int?binCount?=?0;?;?++binCount)?{????????//?遍歷到最后一個(gè)節(jié)點(diǎn)也沒有找到,就新增一個(gè)節(jié)點(diǎn)
????????if?((e?=?p.next)?==?null)?{
??????????p.next?=?newNode(hash,?key,?value,?null);??????????//?如果鏈表長度大于8,則轉(zhuǎn)換為紅黑樹
??????????if?(binCount?>=?TREEIFY_THRESHOLD?-?1)?//?-1?for?1st
????????????treeifyBin(tab,?hash);??????????break;
????????}????????//?找到key對應(yīng)的節(jié)點(diǎn)則跳出遍歷
????????if?(e.hash?==?hash?&&?((k?=?e.key)?==?key?||?(key?!=?null?&&?key.equals(k))))??????????break;
????????p?=?e;
??????}
????}????//?e是最后指向的節(jié)點(diǎn),如果不為空,說明已經(jīng)存在key,則替換舊的value
????if?(e?!=?null)?{?//?existing?mapping?for?key
??????V?oldValue?=?e.value;??????if?(!onlyIfAbsent?||?oldValue?==?null)
????????e.value?=?value;
??????afterNodeAccess(e);??????return?oldValue;
????}
??}??//?新增節(jié)點(diǎn)時(shí)結(jié)構(gòu)改變modCount加1
??++modCount;??if?(++size?>?threshold)
????resize();
??afterNodeInsertion(evict);??return?null;
}

具體過程如圖所示:

JDK源碼分析(5)之 HashMap 相關(guān)

2. resize方法

final?Node<K,V>[]?resize()?{
??Node<K,V>[]?oldTab?=?table;??int?oldCap?=?(oldTab?==?null)???0?:?oldTab.length;??int?oldThr?=?threshold;??int?newCap,?newThr?=?0;??if?(oldCap?>?0)?{????//?如果hash桶已經(jīng)完成初始化,并且已達(dá)最大容量,則直接返回
????if?(oldCap?>=?MAXIMUM_CAPACITY)?{
??????threshold?=?Integer.MAX_VALUE;??????return?oldTab;
????}????//?如果擴(kuò)大2倍沒有超過最大容量,則擴(kuò)大兩倍
????else?if?((newCap?=?oldCap?<<?1)?<?MAXIMUM_CAPACITY?&&?oldCap?>=?DEFAULT_INITIAL_CAPACITY)
??????newThr?=?oldThr?<<?1;?//?double?threshold
??}??//?如果threshold已經(jīng)初始化,則初始化容量為threshold
??else?if?(oldThr?>?0)??????//?initial?capacity?was?placed?in?threshold
????newCap?=?oldThr;??//?如果threshold和哈希桶都沒有初始化,則使用默認(rèn)值
??else?{????????????????????//?zero?initial?threshold?signifies?using?defaults
????newCap?=?DEFAULT_INITIAL_CAPACITY;
????newThr?=?(int)(DEFAULT_LOAD_FACTOR?*?DEFAULT_INITIAL_CAPACITY);
??}??//?重新計(jì)算threshold
??if?(newThr?==?0)?{????float?ft?=?(float)newCap?*?loadFactor;
????newThr?=?(newCap?<?MAXIMUM_CAPACITY?&&?ft?<?(float)MAXIMUM_CAPACITY???(int)ft?:?Integer.MAX_VALUE);
??}
??threshold?=?newThr;??@SuppressWarnings({"rawtypes","unchecked"})
??Node<K,V>[]?newTab?=?(Node<K,V>[])new?Node[newCap];
??table?=?newTab;??if?(oldTab?!=?null)?{????for?(int?j?=?0;?j?<?oldCap;?++j)?{
??????Node<K,V>?e;??????if?((e?=?oldTab[j])?!=?null)?{
????????oldTab[j]?=?null;????????//?如果只有一個(gè)節(jié)點(diǎn),則直接重新放置節(jié)點(diǎn)
????????if?(e.next?==?null)
??????????newTab[e.hash?&?(newCap?-?1)]?=?e;????????//?如果是樹節(jié)點(diǎn),則將紅黑樹拆分后,重新放置
????????else?if?(e?instanceof?TreeNode)
??????????((TreeNode<K,V>)e).split(this,?newTab,?j,?oldCap);????????//?將鏈表拆分為原位置和高位置兩條鏈表
????????else?{?//?preserve?order
??????????Node<K,V>?loHead?=?null,?loTail?=?null;
??????????Node<K,V>?hiHead?=?null,?hiTail?=?null;
??????????Node<K,V>?next;??????????do?{
????????????next?=?e.next;????????????//?節(jié)點(diǎn)重新放置后在原位置
????????????if?((e.hash?&?oldCap)?==?0)?{??????????????if?(loTail?==?null)
????????????????loHead?=?e;??????????????else
????????????????loTail.next?=?e;
??????????????loTail?=?e;
????????????}????????????//?節(jié)點(diǎn)重新放置后位置+oldCap
????????????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
}

上面的擴(kuò)容過程需要注意的是,因?yàn)楣M伴L度總是2的冪,所以在擴(kuò)大兩倍之后原來的節(jié)點(diǎn)只可能在原位置或者原位置+oldCap,具體判斷是通過(e.hash & oldCap) == 0實(shí)現(xiàn)的;

  • 之前將了 & 操作只保留了都是1的有效位

  • oldCap 是2的n次方,實(shí)際也就是在n+1的位置為1,其余地方為0

  • 因?yàn)閿U(kuò)容是擴(kuò)大2倍,實(shí)際上也就是在hash上取了 n+1位,那么就只需要判斷多取的第n+1位是否為0

如圖所示:

JDK源碼分析(5)之 HashMap 相關(guān)

3. get方法

public?V?get(Object?key)?{
??Node<K,V>?e;??return?(e?=?getNode(hash(key),?key))?==?null???null?:?e.value;
}final?Node<K,V>?getNode(int?hash,?Object?key)?{
??Node<K,V>[]?tab;?Node<K,V>?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<K,V>)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;
}

相較于其他方法get方法就要簡單很多了,只是用hash取到對應(yīng)的hash槽,在依次遍歷即可。

4. clone方法

public?Object?clone()?{
??HashMap<K,V>?result;??try?{
????result?=?(HashMap<K,V>)super.clone();
??}?catch?(CloneNotSupportedException?e)?{????//?this?shouldn't?happen,?since?we?are?Cloneable
????throw?new?InternalError(e);
??}
??result.reinitialize();
??result.putMapEntries(this,?false);??return?result;
}

對于clone方法這里有一個(gè)需要注意的地方,result.putMapEntries(this, false),這里在put節(jié)點(diǎn)的時(shí)候是用的this,所以這只是淺復(fù)制,會影響原map,所以在使用的時(shí)候需要注意一下;

至于其他方法還有很多,但大致思路都是一致的,大家可以在看一下源碼。

四、HashMap不同版本對比

1. hash均勻的時(shí)候使用get

Number Of RecordsJava 5Java 6Java 7Java 8
10,0004 ms3 ms4 ms2 ms
100,0007 ms6 ms8 ms4 ms
1,000,00099 ms15 ms14 ms13 ms

2. hash不均勻的時(shí)候使用get

Number Of RecordsJava 5Java 6Java 7Java 8
10,000197 ms154 ms132 ms15 ms
100,00030346 ms18967 ms19131 ms177 ms
1,000,0003716886 ms2518356 ms2902987 ms1226 ms
10,000,000OOMOOMOOM5775 ms

3. hash均勻的時(shí)候使用put

Number Of RecordsJava 5Java 6Java 7Java 8
10,00017 ms12 ms13 ms10 ms
100,00045 ms31 ms34 ms46 ms
1,000,000384 ms72 ms66 ms82 ms
10,000,0004731 ms944 ms1024 ms99 ms

4. hash不均勻的時(shí)候使用put

Number Of RecordsJava 5Java 6Java 7Java 8
10,000211 ms153 ms162 ms10 ms
100,00029759 ms17981 ms17653 ms93 ms
1,000,0003527633 ms2509506 ms2902987 ms333 ms
10,000,000OOMOOMOOM3970 ms

從以上對比可以看到 JDK8 的 HashMap 無論 hash 是否均勻效率都要好得多,這里面hash算法的改良功不可沒,并且因?yàn)榧t黑樹的引入使得它在hash不均勻甚至在所有key的hash都相同的情況,任然表現(xiàn)良好。

總結(jié)

  1. 擴(kuò)容需要重排所有節(jié)點(diǎn)特別損耗性能,所以估算map大小并給定一個(gè)合理的負(fù)載系數(shù),就顯得尤為重要了。

  2. HashMap 是線程不安全的。

  3. 雖然 JDK8 中引入了紅黑樹,將極端hash的情況影響降到了最小,但是從上面的對比還是可以看到,一個(gè)好的hash對性能的影響仍然十分重大,所以寫一個(gè)好的hashCode()也非常重要。


向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI