昨天介紹了 HashMap 將 key 定位到 bucket 的過程。而本文主要解說的是,在一個 bucket 中,要如何找到 key 所對應的節點,進而討論為什麼 hashCode
和 equals
方法要一起覆寫。最後介紹 HashMap 的容量與擴充。
此篇亦轉載到個人部落格。
幫讀者做個小複習,HashMap 在 put
或 get
資料時,會根據傳入的 key
參數,定位出資料應在的 bucket,也就是 table
陣列的某個位置。過程中會幫 key
算出一個 HashMap 的專用雜湊值,再換算成陣列 index。
然而發生雜湊碰撞在所難免,假使 bucket 中已經有節點資料了,那就要在這些節點中逐一確認。這會透過 equals
方法,好好地比對 key 的內容是否相等。
若正在 put
資料,當找到 key 相等的節點,就進行覆蓋;否則就插入一個新節點。
若正在 get
資料,當找到 key 相等的節點,就回傳;否則回傳 null。
對於 hashCode
和 equals
方法要一起覆寫的議題,便是跟上述的操作有關。
如果沒有覆寫 hashCode
,並預期兩個內容相同的物件會被 HashSet
視為相同,那就錯了。由於 hashCode
預設是根據記憶體位址算出,第二個物件可能就直接被放入不同的 bucket 了。即便有覆寫 equals
,也沒有執行的機會。
如果只覆寫了 hashCode
,卻忘記覆寫 equals
,那麼即使內容相同的物件被算出在同一個 bucket,但由於 equals
預設是判斷記憶體位址相等,它們在最終依然被視為不同。
在宣告 HashMap
時,可以透過建構子傳入 initialCapacity
(初始容量)參數,用來指定 bucket 的初始數量(即 table
陣列長度)。另外還有一個參數叫 loadFactor
(負載因子)。我們稱 initialCapacity × loadFactor
為「閥值」(threshold)。
HashMap 有個特別的設計,即使建構子的 initialCapacity
參數不是傳入 2 的冪數,仍然會被換算為大於它的冪數。例如傳入 15,就換算成 16;傳入 17,就換算成 32。
為何刻意取 2 的冪數呢?。要知道在定位 bucket 時,key
的專用雜湊值會與 bucket 數量 - 1
進行 AND 位元運算,以求得 bucket 的位置。又因 2 的冪數 - 1
的二進制都是 1(如 16 - 1 = 15 = 1111
),故 0000 ~ 1111 的每個數字都有機會算出來,有利於減少雜湊碰撞。筆者就不再贅述昨天提過的細節。
當存放完新資料後,若發現已超過閥值,HashMap 會進行擴充容量的動作,簡稱擴容。要做的事包含:
table
陣列的長度,增加為原來的 2 倍。table
陣列中。也就是說,initialCapacity
預設為 16,且 loadFactor
預設為 0.75,則當資料數量超過 12 時,便會啟動擴容機制。使得 bucket 數量增至 32,而新的閥值為 24。
超過閥值這件事,對 HashMap 而言意味著雜湊碰撞變嚴重了,這可以從兩個角度來思考。
HashMap 擴容之後,table 陣列長度 - 1
的二進制,會多一個 1。藉此重新計算出每一筆資料節點要存放的新位置,達到「打散」的效果。仔細想想,其實 HashMap 的 bucket 並非每一個都會被用到。它的設計者犧牲了空間,來換取效率。
擴容是一個重新安置所有資料的操作,會消耗較大的效能。所以當我們能預測存進 HashMap 的資料數量有多少時,建議可在宣告時給予適當的 initialCapacity
,避免執行太多次的擴容。
Ref:
HashMap 什麼時候進行擴容呢
面渣逆襲:HashMap追魂二十三問
今日文章到此結束!
最後推廣一下自己的部落格,我是「新手工程師的程式教室」的作者,請多指教