HashMap 实现原理
轉(zhuǎn)載自?HashMap 實(shí)現(xiàn)原理
HashMap是??键c(diǎn),而一般不問(wèn)List的幾個(gè)實(shí)現(xiàn)類(偏簡(jiǎn)單)。以下基于JDK1.8.0_102分析。
內(nèi)部存儲(chǔ)
HashMap的內(nèi)部存儲(chǔ)是一個(gè)數(shù)組(bucket),數(shù)組的元素Node實(shí)現(xiàn)了是Map.Entry接口(hash, key, value, next),next非空時(shí)指向定位相同的另一個(gè)Entry,如圖:
容量(capacity)和負(fù)載因子(loadFactor)
簡(jiǎn)單的說(shuō),capacity就是bucket的大小,loadFactor就是bucket填滿程度的最大比例。當(dāng)bucket中的entries的數(shù)目(而不是已占用的位置數(shù))大于capacity*loadFactor時(shí)就需要擴(kuò)容,調(diào)整bucket的大小為當(dāng)前的2倍。同時(shí),初始化容量的大小也是2的次冪(大于等于設(shè)定容量的最小次冪),則bucket的大小在擴(kuò)容前后都將是2的次冪(非常重要,resize時(shí)能帶來(lái)極大便利)。
Tips:
默認(rèn)的capacity為16,loadFactor為0.75,但如果需要優(yōu)化的話,要考量具體的使用場(chǎng)景。
- 如果對(duì)迭代性能要求高,不要把capacity設(shè)置過(guò)大,也不要把loadFactor設(shè)置過(guò)小,否則會(huì)導(dǎo)致bucket中的空位置過(guò)多,浪費(fèi)性能
- 如果對(duì)隨機(jī)訪問(wèn)的性能要求很高的話,不要把loadFactor設(shè)置的過(guò)大,否則會(huì)導(dǎo)致訪問(wèn)時(shí)頻繁碰撞,時(shí)間復(fù)雜度向O(n)退化
- 如果數(shù)據(jù)增長(zhǎng)很快的話,或數(shù)據(jù)規(guī)??深A(yù)知,可以在創(chuàng)建HashMap時(shí)主動(dòng)設(shè)置capacity
hash與定位
作為API的設(shè)計(jì)者,不能假定用戶實(shí)現(xiàn)了良好的hashCode方法,所以通常會(huì)對(duì)hashCode再計(jì)算一次hash:
| 1234 | static final int hash(Object key) {int h;return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);} |
hashCode方法
注意key.hashCode()的多態(tài)用法。重點(diǎn)是hash方法。
hash方法的實(shí)現(xiàn)和定位
前面已經(jīng)說(shuō)過(guò),在get和put計(jì)算下標(biāo)時(shí),先對(duì)hashCode進(jìn)行hash操作,然后再通過(guò)hash值進(jìn)一步計(jì)算下標(biāo),如下圖所示:
回顧hash方法的源碼可知,hash方法大概的作用就是:高16bit不變,低16bit和高16bit做了一個(gè)異或。
javadoc這樣說(shuō):
Computes key.hashCode() and spreads (XORs) higher bits of hash to lower. Because the table uses power-of-two masking, sets of hashes that vary only in bits above the current mask will always collide. (Among known examples are sets of Float keys holding consecutive whole numbers in small tables.) So we apply a transform that spreads the impact of higher bits downward. There is a tradeoff between speed, utility, and quality of bit-spreading. Because many common sets of hashes are already reasonably distributed (so don’t benefit from spreading), and because we use trees to handle large sets of collisions in bins, we just XOR some shifted bits in the cheapest possible way to reduce systematic lossage, as well as to incorporate impact of the highest bits that would otherwise never be used in index calculations because of table bounds.
在設(shè)計(jì)hash函數(shù)時(shí),因?yàn)槟壳暗膖able長(zhǎng)度n為2的次冪,所以計(jì)算下標(biāo)的時(shí)候,可使用按位與&代替取模%:
| 1 | (n - 1) & hash |
設(shè)計(jì)者認(rèn)為這方法很容易發(fā)生碰撞。為什么這么說(shuō)呢?不妨思考一下,在n – 1為15(0×1111)時(shí),散列真正生效的只是低4bit的有效位,當(dāng)然容易碰撞了。
因此,設(shè)計(jì)者想了一個(gè)顧全大局的方法(綜合考慮了速度、作用、質(zhì)量),就是把高16bit和低16bit異或了一下。設(shè)計(jì)者還解釋到因?yàn)楝F(xiàn)在大多數(shù)的hashCode的分布已經(jīng)很不錯(cuò)了,就算是發(fā)生了碰撞也用O(logn)的tree去做了。僅僅異或一下,既減少了系統(tǒng)的開(kāi)銷,也不會(huì)造成因?yàn)楦呶粵](méi)有參與下標(biāo)的計(jì)算(table長(zhǎng)度比較小)時(shí),引起的碰撞。
但我沒(méi)有理解為什么“很”容易發(fā)生碰撞。如此設(shè)計(jì)的話,hash的分布是均勻的,且極其簡(jiǎn)單;將高16bit與低16bit異或之后,hash的分布變的復(fù)雜一些,更“接近”隨機(jī),但仍然是均勻的。估計(jì)作者是從實(shí)際使用的角度出發(fā),因?yàn)橐话闱闆r下,key的分布也符合“局部性原理”,低比特位相同的概率大于異或后仍然相同的概率,從而降低了碰撞的概率。
碰撞
調(diào)用put方法時(shí),盡管我們?cè)O(shè)法避免碰撞以提高HashMap的性能,還是可能發(fā)生碰撞。據(jù)說(shuō)碰撞率還挺高,平均加載率到10%時(shí)就會(huì)開(kāi)始碰撞。我們使用開(kāi)放散列法來(lái)處理碰撞節(jié)點(diǎn)。
將舊entry的引用賦值給新entry的next屬性,改將新entry放在該位置——即在該位置上存儲(chǔ)一個(gè)鏈表,沖突節(jié)點(diǎn)從鏈表頭部插入,這樣插入新entry時(shí)不需要遍歷鏈表,時(shí)間復(fù)雜度為O(1)。但如果鏈表過(guò)長(zhǎng),查詢性能仍將退化到O(n)。Java8中對(duì)鏈表長(zhǎng)度增加了一個(gè)閾值,超過(guò)閾值鏈表將轉(zhuǎn)化為紅黑樹(shù),查詢時(shí)間復(fù)雜度降為O(logn),提高了鏈表過(guò)長(zhǎng)時(shí)的性能。
調(diào)用get方法時(shí),定位到該位置,再遍歷紅黑樹(shù),比較key值找到所需元素:
| 12345678910111213141516171819 | 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;} |
判斷元素相等的設(shè)計(jì)比較經(jīng)典,利用了bool表達(dá)式的短路特性:先比較hash值;如果hash值相等,就通過(guò)==比較;如果==不等,再通過(guò)equals方法比較。hash是提前計(jì)算好的;如果沒(méi)有重載運(yùn)算符(通常也不建議這樣做),==一般直接比較引用值;equals方法最有可能耗費(fèi)性能,如String的equals方法需要O(n)的時(shí)間,n是字符串長(zhǎng)度。一定要記住這里的判斷順序,很能考察對(duì)碰撞處理源碼的理解。
針對(duì)HashMap的使用,此處要注意覆寫hashCode和equals方法時(shí)的兩個(gè)重點(diǎn):
- 覆寫后,一定要保證equals判斷相等的時(shí)候,hashCode的返回值也相等。
- 對(duì)于選作key的類,要保證調(diào)用put與get時(shí)hashCode的返回值相等,equals的性質(zhì)相同。
resize
resize是HashMap中最難理解的部分。
調(diào)用put方法時(shí),如果發(fā)現(xiàn)目前的bucket占用程度已經(jīng)超過(guò)了loadFactor,就會(huì)發(fā)生resize。簡(jiǎn)單的說(shuō)就是把bucket擴(kuò)充為2倍,之后重新計(jì)算index,把節(jié)點(diǎn)再放到新的bucket中。
javadoc中這樣說(shuō):
Initializes or doubles table size. If null, allocates in accord with initial capacity target held in field threshold. Otherwise, because we are using power-of-two expansion, the elements from each bin must either stay at same index, or move with a power of two offset in the new table.
即,當(dāng)超過(guò)限制的時(shí)候會(huì)resize,又因?yàn)槲覀兪褂玫氖?次冪的擴(kuò)展,所以,元素的位置要么是在原位置,要么是在原位置再移動(dòng)2次冪的位置。
怎么理解呢?例如我們從16擴(kuò)展為32時(shí),具體的變化如下:
假設(shè)bucket大小n=2^k,元素在重新計(jì)算hash之后,因?yàn)閚變?yōu)?倍,那么新的位置就是(2^(k+1)-1)&hash。而2^(k+1)-1=2^k+2^k-1,相當(dāng)于2^k-1的mask范圍在高位多1bit(紅色)(再次提醒,原來(lái)的長(zhǎng)度n也是2的次冪),這1bit非1即0。如圖:
所以,我們?cè)趓esize的時(shí)候,不需要重新定位,只需要看看原來(lái)的hash值新增的那個(gè)bit是1還是0就好了,是0的話位置沒(méi)變,是1的話位置變成“原位置+oldCap”。代碼比較長(zhǎng)就不貼了,下面為16擴(kuò)充為32的resize示意圖:
這個(gè)設(shè)計(jì)非常的巧妙,新增的1bit是0還是1可以認(rèn)為是隨機(jī)的,因此resize的過(guò)程均勻的把之前的沖突的節(jié)點(diǎn)分散到新的bucket中了。
參考:
- Java HashMap工作原理及實(shí)現(xiàn) | Yikun
總結(jié)
以上是生活随笔為你收集整理的HashMap 实现原理的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: Java8系列之重新认识HashMap
- 下一篇: 图解HashMap和HashSet的内部