Java集合之HashMap源码分析
以下源碼均為jdk1.7
HashMap概述
HashMap是基于哈希表的Map接口的非同步實(shí)現(xiàn). 提供所有可選的映射操作, 并允許使用null值和null健. 此類不保證映射的順序.
需要注意的是: HashMap不是同步的.
哈希表
哈希表定義: 哈希表是一種根據(jù)關(guān)鍵碼去尋找值的數(shù)據(jù)映射結(jié)構(gòu), 該結(jié)構(gòu)通過把關(guān)鍵碼映射的位置去尋找存放值的地方.
舉個例子, 最典型的例子就是字典, 如果想要在字典中查找"按"字, 通常會根據(jù)拼音 an 去查找拼音索引(當(dāng)然也可以是偏旁索引), 然后找到 ti 在字典中的位置, 得到第一個拼音為 an 的字 "安". 這個過程就是鍵碼映射, 即 通過 key 查找 f(key). 其中 key為關(guān)鍵字, f()是哈希函數(shù), 哈希函數(shù)的結(jié)果就是哈希值.
哈希沖突:?那么問題來了, 我們要查找的是"按",而不是"安", 但他們的拼音都是一樣的. 通過關(guān)鍵字 an "按"和"安"可以映射到一樣的字典頁碼4的位置, 這就是哈希沖突(也叫哈希碰撞), 在公式上表達(dá)就是 key1 != key2, 但f(key1)=f(key2).
key 為值, f(key)計(jì)算得出數(shù)組中存儲地址, 這樣就會出現(xiàn)兩個元素的地址相同的情況. 這時, 哈希函數(shù)的設(shè)計(jì)就至關(guān)重要了, 好的哈希函數(shù)會盡可能的保證 計(jì)算簡單和散列地址分布均勻, 但是, 數(shù)組是一個連續(xù)的固定長度的內(nèi)存空間, 再好的哈希函數(shù)也不能保證得到的存儲地址絕不發(fā)生沖突.
哈希沖突的解決方案有多種: 開放定址法(發(fā)生沖突, 尋找下一個), 再散列函數(shù)法, 鏈地址法.
HashMap就是采用了鏈地址發(fā), 也就是 數(shù)組+鏈表 的方式.
HashMap的實(shí)現(xiàn)原理
最基本的數(shù)據(jù)結(jié)構(gòu)有兩種: 數(shù)組和指針, HashMap就是通過這兩個數(shù)據(jù)結(jié)構(gòu)實(shí)現(xiàn)的, 是數(shù)組和鏈表的結(jié)合體.
?
從圖中可以看出, HashMap底層是一個數(shù)組結(jié)構(gòu), 數(shù)組中的每一項(xiàng)是一個鏈表. 當(dāng)新建HashMap時, 會初始化一個數(shù)組.
HashMap的主干是一個Entry數(shù)組.
?
Entry是一個靜態(tài)內(nèi)部類, 包含 key-value.
?
HashMap存儲的整體結(jié)構(gòu)如下:
?
簡單說, HashMap有數(shù)組+鏈表組成, 數(shù)組是HashMap的主體, 鏈表是為了解決哈希沖突而存在的, 如果定位到數(shù)組位置不含鏈表(當(dāng)前entry的next指向null), 那么對于查找,添加等操作很快, 僅需一次尋址即可; 如果定位到的數(shù)組包含鏈表, 那么添加操作就要遍歷鏈表, 然后通過key的equals方法進(jìn)行逐一對比, 存在即覆蓋, 不存在則新增, 而查找操作也需遍歷鏈表.
所以, 性能考慮, HashMap中的鏈表出現(xiàn)越少, 性能越好.
HasmMap幾個重要的字段:
?
?
?
?
?
HashMap的構(gòu)造函數(shù):
?
從上面代碼中可以看出, 在常規(guī)構(gòu)造器中, 沒有為數(shù)組 table 分配內(nèi)存空間(有個參數(shù)為map的構(gòu)造器除外), 而是在執(zhí)行 put操作時才真正構(gòu)建table數(shù)組
?
再來看 inflateTable()方法源碼:
?
重量級角色, 哈希函數(shù)出場:
?
indexFor()函數(shù)實(shí)現(xiàn)如下:
?
h&(length - 1)保證獲取的index一定在數(shù)組的范圍內(nèi), 例如: 容量為16, length-1=15, h=18, 進(jìn)行計(jì)算為:
?
得出index=2.
故而, 最終存儲位置的確定為如下流程:
?
最后看下 addEntry 的實(shí)現(xiàn):
?
通過 addEntry 的代碼可以看出, 當(dāng)發(fā)生哈希沖突并且size大于閾值時, 需要進(jìn)行數(shù)組擴(kuò)容, 擴(kuò)容時, 需要新建一個長度為之前2倍的新數(shù)組, 最后將當(dāng)前的Entry數(shù)組中元素全部傳過去, 擴(kuò)容后的新數(shù)組長度為之前的2倍, 所以擴(kuò)容相對來說是一個耗資源的操作.
下面看get方法就簡單得多了:
?
然后是getEntry()源碼:
?
可以看出, get方法的實(shí)現(xiàn)相當(dāng)簡單, 流程為: key(hashcode)-->hash-->indexFor-->最終索引位置, 找到對應(yīng)位置table[i], 在查看是否有鏈表, 遍歷鏈表, 通過key的equals方法比對查找對應(yīng)的記錄.
在getEntry方法中, 定位到數(shù)組位置之后遍歷鏈表的時候, e.hash==hash這個判斷是否有必要. 試想如下場景, 如果傳入的key對象重寫了equals方法卻沒有重寫hashCode, 而恰巧此對象定位到這個數(shù)組位置, 如果僅僅用equals判斷可能是相等的, 但其hashCode和當(dāng)前對象不一致, 這種情況, 根據(jù)Object的hashCode的約定, 不能返回當(dāng)前對象, 而應(yīng)該返回null.
重寫equals方法要同時重寫hashCode方法
為什么重寫equals時也要同時重寫hashCode? 下面舉個小例子:
?
實(shí)際輸出結(jié)果:
結(jié)果: null
現(xiàn)在我們已經(jīng)對HashMap的原理有了一定了解, 這個結(jié)果就不難理解了. 盡管我們在進(jìn)行g(shù)et和put操作的時候, 使用的key從邏輯上講是等值的, 但由于沒有重寫hashCode方法, 在進(jìn)行put操作時: key(hashcod1)-->hash-->indexFor-->最終索引位置; 而通過key去除value時: key(hashcode2)-->hash-->indexFor-->最終索引位置, 由于hashcode1和hashcode2不相等, 最終得出的數(shù)組索引頁不一樣而返回null(也可能碰巧定位到了一個數(shù)組位置, 但是也會判斷其entry的hash值是否相等, 上面get方法中有提到)
所以, 在重寫equals方法時, 必須注意重寫hashCode方法, 同時還要保證equals判斷相等的兩個對象, 調(diào)用hashCode方法要返回同樣的整數(shù)值, 而equals判斷不相等的兩個對象, 其hashCode可以相同, 只是會發(fā)生哈希沖突, 應(yīng)該盡量避免.
HashMap的遍歷
?
總結(jié)
HashMap底層將key-value當(dāng)成一個整體處理, 這個整體就是Entry對象. HashMap底層采用一個Entry[]數(shù)組來保存所有的key-value對, 當(dāng)需要存儲一個Entry對象時, 會根據(jù)hash算法來決定其在數(shù)組中的位置, 再根據(jù)equals方法決定其在該數(shù)組位置上的鏈表中的存儲位置; 當(dāng)需要取出一個Entry時, 也會根據(jù)hash算法找到其在數(shù)組中的存儲位置, 再根據(jù)equals方法從該位置上的鏈表中取出該Entry.
創(chuàng)作挑戰(zhàn)賽新人創(chuàng)作獎勵來咯,堅(jiān)持創(chuàng)作打卡瓜分現(xiàn)金大獎總結(jié)
以上是生活随笔為你收集整理的Java集合之HashMap源码分析的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: java邮箱exchange_使用Jav
- 下一篇: 快速排序 java导包_排序算法-快速排