1. 程式人生 > >Java集合之HashMap原始碼分析

Java集合之HashMap原始碼分析

 

前面我們提到了集合,今天我們就具體來了解一下Java集合中具體的組成部分!

Java集合之HashMap原始碼分析

 

以下原始碼均為jdk1.7

HashMap概述

HashMap是基於雜湊表的Map介面的非同步實現. 提供所有可選的對映操作, 並允許使用null值和null健. 此類不保證對映的順序.

需要注意的是: HashMap不是同步的.

雜湊表

雜湊表定義: 雜湊表是一種根據關鍵碼去尋找值的資料對映結構, 該結構通過把關鍵碼對映的位置去尋找存放值的地方.

舉個例子, 最典型的例子就是字典, 如果想要在字典中查詢"按"字, 通常會根據拼音 an 去查詢拼音索引(當然也可以是偏旁索引), 然後找到 ti 在字典中的位置, 得到第一個拼音為 an 的字 "安". 這個過程就是鍵碼對映, 即 通過 key 查詢 f(key). 其中 key為關鍵字, f()是雜湊函式, 雜湊函式的結果就是雜湊值.

雜湊衝突: 那麼問題來了, 我們要查詢的是"按",而不是"安", 但他們的拼音都是一樣的. 通過關鍵字 an "按"和"安"可以對映到一樣的字典頁碼4的位置, 這就是雜湊衝突(也叫雜湊碰撞), 在公式上表達就是 key1 != key2, 但f(key1)=f(key2).

key 為值, f(key)計算得出陣列中儲存地址, 這樣就會出現兩個元素的地址相同的情況. 這時, 雜湊函式的設計就至關重要了, 好的雜湊函式會盡可能的保證 計算簡單和雜湊地址分佈均勻, 但是, 陣列是一個連續的固定長度的記憶體空間, 再好的雜湊函式也不能保證得到的儲存地址絕不發生衝突.

雜湊衝突的解決方案有多種: 開放定址法(發生衝突, 尋找下一個), 再雜湊函式法, 鏈地址法.

HashMap就是採用了鏈地址發, 也就是 陣列+連結串列 的方式.

HashMap的實現原理

最基本的資料結構有兩種: 陣列和指標, HashMap就是通過這兩個資料結構實現的, 是陣列和連結串列的結合體.

Java集合之HashMap原始碼分析

 

從圖中可以看出, HashMap底層是一個數組結構, 陣列中的每一項是一個連結串列. 當新建HashMap時, 會初始化一個數組.

HashMap的主幹是一個Entry陣列.

Java集合之HashMap原始碼分析

 

Entry是一個靜態內部類, 包含 key-value.

Java集合之HashMap原始碼分析

 

HashMap儲存的整體結構如下:

Java集合之HashMap原始碼分析

 

簡單說, HashMap有陣列+連結串列組成, 陣列是HashMap的主體, 連結串列是為了解決雜湊衝突而存在的, 如果定位到陣列位置不含連結串列(當前entry的next指向null), 那麼對於查詢,新增等操作很快, 僅需一次定址即可; 如果定位到的陣列包含連結串列, 那麼新增操作就要遍歷連結串列, 然後通過key的equals方法進行逐一對比, 存在即覆蓋, 不存在則新增, 而查詢操作也需遍歷連結串列.

所以, 效能考慮, HashMap中的連結串列出現越少, 效能越好.

HasmMap幾個重要的欄位:

Java集合之HashMap原始碼分析

 

Java集合之HashMap原始碼分析

 

Java集合之HashMap原始碼分析

 

Java集合之HashMap原始碼分析

 

Java集合之HashMap原始碼分析

 

HashMap的建構函式:

Java集合之HashMap原始碼分析

 

從上面程式碼中可以看出, 在常規構造器中, 沒有為陣列 table 分配記憶體空間(有個引數為map的構造器除外), 而是在執行 put操作時才真正構建table陣列

Java集合之HashMap原始碼分析

 

再來看 inflateTable()方法原始碼:

Java集合之HashMap原始碼分析

 

重量級角色, 雜湊函數出場:

Java集合之HashMap原始碼分析

 

indexFor()函式實現如下:

Java集合之HashMap原始碼分析

 

h&(length - 1)保證獲取的index一定在陣列的範圍內, 例如: 容量為16, length-1=15, h=18, 進行計算為:

Java集合之HashMap原始碼分析

 

得出index=2.

故而, 最終儲存位置的確定為如下流程:

Java集合之HashMap原始碼分析

 

最後看下 addEntry 的實現:

Java集合之HashMap原始碼分析

 

通過 addEntry 的程式碼可以看出, 當發生雜湊衝突並且size大於閾值時, 需要進行陣列擴容, 擴容時, 需要新建一個長度為之前2倍的新陣列, 最後將當前的Entry陣列中元素全部傳過去, 擴容後的新陣列長度為之前的2倍, 所以擴容相對來說是一個耗資源的操作.

下面看get方法就簡單得多了:

Java集合之HashMap原始碼分析

 

然後是getEntry()原始碼:

Java集合之HashMap原始碼分析

 

可以看出, get方法的實現相當簡單, 流程為: key(hashcode)-->hash-->indexFor-->最終索引位置, 找到對應位置table[i], 在檢視是否有連結串列, 遍歷連結串列, 通過key的equals方法比對查詢對應的記錄.

在getEntry方法中, 定位到陣列位置之後遍歷連結串列的時候, e.hash==hash這個判斷是否有必要. 試想如下場景, 如果傳入的key物件重寫了equals方法卻沒有重寫hashCode, 而恰巧此物件定位到這個陣列位置, 如果僅僅用equals判斷可能是相等的, 但其hashCode和當前物件不一致, 這種情況, 根據Object的hashCode的約定, 不能返回當前物件, 而應該返回null.

重寫equals方法要同時重寫hashCode方法

為什麼重寫equals時也要同時重寫hashCode? 下面舉個小例子:

Java集合之HashMap原始碼分析

 

實際輸出結果:

結果: null

現在我們已經對HashMap的原理有了一定了解, 這個結果就不難理解了. 儘管我們在進行get和put操作的時候, 使用的key從邏輯上講是等值的, 但由於沒有重寫hashCode方法, 在進行put操作時: key(hashcod1)-->hash-->indexFor-->最終索引位置; 而通過key去除value時: key(hashcode2)-->hash-->indexFor-->最終索引位置, 由於hashcode1和hashcode2不相等, 最終得出的陣列索引頁不一樣而返回null(也可能碰巧定位到了一個數組位置, 但是也會判斷其entry的hash值是否相等, 上面get方法中有提到)

所以, 在重寫equals方法時, 必須注意重寫hashCode方法, 同時還要保證equals判斷相等的兩個物件, 呼叫hashCode方法要返回同樣的整數值, 而equals判斷不相等的兩個物件, 其hashCode可以相同, 只是會發生雜湊衝突, 應該儘量避免.

HashMap的遍歷

Java集合之HashMap原始碼分析

 

總結

HashMap底層將key-value當成一個整體處理, 這個整體就是Entry物件. HashMap底層採用一個Entry[]陣列來儲存所有的key-value對, 當需要儲存一個Entry物件時, 會根據hash演算法來決定其在陣列中的位置, 再根據equals方法決定其在該陣列位置上的連結串列中的儲存位置; 當需要取出一個Entry時, 也會根據hash演算法找到其在陣列中的儲存位置, 再根據equals方法從該位置上的連結串列中取出該Entry.

Java。大家都知道,我們是學Java全棧的,大家就肯定以為我有全套的Java系統教程。沒錯,我是有Java全套系統教程,進扣裙【47】974【9726】所示,!~

Java集合之HashMap原始碼分析

 

“我們相信人人都可以成為一個程式設計師,現在開始,找個師兄,帶你入門,學習的路上不再迷茫。這裡是ja+va修真院,初學者轉行到網際網路行業的聚集地。"