JDK1 7HashMap原始碼解析

2021-09-23 10:49:59 字數 4893 閱讀 9830

hashmap已經看了很多篇文章了,今天還是自己解析一遍吧。

我先大致介紹下hashmap的內部結構再跟著原始碼解讀一番

眾所周知hashmap的內部就是乙個雜湊表

什麼是雜湊表?

如果我們利用陣列可隨機訪問的特性,將要存入的鍵通過一種雜湊演算法轉換成乙個數字,並把這個數字轉換成陣列的下標,然後將鍵和他對應value放入陣列中。那麼我們再通過這個鍵查詢時,只需要繼續利用這個演算法,那麼我們就只需要用o(1)的時間複雜度就可以迅速找到這個鍵在陣列中的位置從而找到鍵對應的value。

不過o(1)只是理想情況 為什麼?

因為我們可以存入無數種可能的鍵,但是這個雜湊演算法 算出來的數字 是有限的,並且還需要轉換成陣列的下標,所以不同鍵轉換的下標值就會有很大可能相同,這種問題叫做雜湊衝突

怎麼解決雜湊衝突?

舉個例子:

陣列 array[4] 處已經有乙個鍵值對 「wmh」-20,

此時想再放乙個鍵值對"wyy"-18 不幸的是 "wyy"這個鍵轉換的下標也是4

那麼怎麼辦呢?

我們只需要將新的鍵值對放在原先鍵值對的後面形成乙個單鏈表來儲存就可以了

這種解決雜湊衝突的辦法叫做鏈位址法

所以我們就知道如果我們訪問陣列的某個位置中有多個鍵值形成了單鏈表時,我們還需要遍歷這個鍊錶找到我們所需要的鍵值對,此時的時間複雜度就不是o(1)了而是o(n)

如果因為雜湊衝突過多導致乙個單鏈表中的結點有很多時,我們查詢的速度就會大大下降,所以在適當的時候我們必須將原先陣列中的鍵值對移到乙個更大的陣列中 這個動作叫做擴容

適當的時候?指的是陣列中的鍵值對到達一定的數量的時候(閥值)

hashmap中定義了乙個變數叫做載入因子

閥值 = 陣列大小 * 載入因子

當我們每次往hashmap中put乙個元素時就會檢查當前鍵值對數量是否大於閥值,如果大於閥值則會進行擴容動作

怎麼擴容?

我們需要遍歷老陣列中的元素 並且遍歷每個新的單鏈表 將每個鍵值對重新雜湊後計算他們在新陣列中的位置,全部放入新陣列後,再將老陣列指向新的陣列。

這個擴容的速度是十分慢的,所以定義乙個正確的載入因子十分重要

當載入因子過大,這意味著單鏈表可能會越長 查詢乙個鍵值對的速度會變慢

當載入因子過小,這意味著陣列還有很多空間就會進行擴容,這樣又會浪費大量的儲存空間

hashmap選擇了乙個最適合的載入因子為0.75

主要解析 構造器 put resize這幾個方法

先來看下hashmap中有哪些成員變數

// 預設初始化容量

static

final

int default_initial_capacity =16;

// 最大容量

static

final

int maximum_capacity =

1<<30;

// 預設的載入因子

static

final

float default_load_factor =

0.75f

;// entry陣列

transient entry[

] table;

// key-vlue結點數量

transient

int size;

// 需要擴容時的容量 (閥值 = 容量 / 載入因子)

int threshold;

// 載入因子

final

float loadfactor;

// 被修改的次數

transient

volatile

int modcount;

static

class

entry

implements

map.entry

成員變數中包含上面介紹中所提到的各種,比如陣列、載入因子、閥值

hashmap用乙個靜態內部類entry表示 乙個鍵值對,即entry陣列中的乙個結點,每個結點中又有乙個hash變數表示 鍵的hash值,和乙個next指標指向單鏈表中的下乙個結點

先來看構造方法

當我們new乙個無參的hashmap時,會初始化預設載入因子0.75 和乙個預設容量16 的entry陣列

public

hashmap()

當我們new乙個帶指定容量和載入因子的hashmap時

首先檢查2個值是否合法

把傳進來的容量變成最接近這個容量的2的n次方倍計算閥值

初始化陣列

可以發現2點:

無論我們指定多大的容量,這個容量都會變成乙個2的n次方的數

1.7的hashmap並沒有進行延遲初始化

public

hashmap

(int initialcapacity,

float loadfactor)

接下來看put方法:不想解釋了 直接看注釋。。

不過需要注意幾點

當鍵為null時 直接插入到陣列下標為0的位置中hashmap沒有直接把鍵的hashcode值轉換成下標,會將hashcode值經過一次再雜湊的過程,再轉換成下標至於為什麼,在下面可以看到

結點插入到鍊錶中不是在尾部追加,而是直接在頭部插入的這樣插入的速度是很快的,不需要遍歷到鍊錶尾部進行插入

public v put

(k key, v value)

}// 修改次數加1

modcount++

;// 進行到這裡 有2種情況

// 1.該位置上根本就沒有結點

// 2.該位置結點形成的鍊錶中沒有與要插入的鍵相等的鍵

// 這兩種情況都直接往該位置上建立新的結點 next指標指向原來該位置上的結點

// 如果該位置上有結點 就說明是在這個鍊錶的頭部插入的。

addentry

(hash, key, value, i)

;return null;

}

計算下標方法

這個方法將鍵的雜湊值和 陣列的長度-1 按位相與得到陣列下標,為什麼不直接和長度取模計算呢?

利用位運算代替取模運算,可以大大提高程式的計算效率。位運算可以直接對記憶體資料進行操作,不需要轉換成十進位制,因此效率要高得多。

但是這個位運算 只有在陣列的長度是2的n次方的時候 才會有效

所以這個陣列的長度永遠會被保證在2的n次方

// 計算該鍵在entry陣列中對應的位置 (h為該鍵對應的雜湊值)

// 當 length為2的n次方時 等價於 h % length

static

intindexfor

(int h,

int length)

再雜湊方法

為什麼要經過這個再雜湊? 直接拿鍵的hashcode值計算下標不就可以了嗎?

因為如果直接拿鍵的hashcode值計算 雜湊衝突的概率會大大增加,

當陣列長度不高時,hashcode值和陣列長度-1 按位相與的時候 只會和hashcode值的低位相關

高位參與不進來 雜湊衝突大大增加

經過這個擾動函式進行再雜湊後,會使hashcode值的高位參與計算,雜湊衝突概率大大降低

static

inthash

(int h)

注釋寫的很清楚 主要看transfer方法:

transfer方法 會遍歷老陣列 並且遍歷每個結點形成的鍊錶,將每個結點重新雜湊計算他們在新陣列的下標

然後直接將結點的next指標指向老陣列的下標位置上的結點。

所以可以發現

原來結點所形成的鍊錶,如果這些結點恰好在新陣列中還是乙個位置,那麼相對原來是逆序連線的

void

resize

(int newcapacity)

// 建立新的容量的陣列

entry[

] newtable =

newentry

[newcapacity]

;// 將老陣列中的結點 重新雜湊計算下標 放入新的陣列中

transfer

(newtable)

;// 將老的陣列指向新的陣列

table = newtable;

// 新的閥值 = 新的容量 * 載入因子

threshold =

(int

)(newcapacity * loadfactor);}

void

transfer

(entry[

] newtable)

while

(e != null);}

}}

JDK1 7 HashMap原始碼解析

在jdk1.7中,hashmap底層資料結構是由陣列和鍊錶構成,陣列存放entry物件,而entry物件是對鍊錶頭部第乙個元素的引用。arraylist中add方法是通過下標的累加進行資料的插入,hashmap則不同。hashmap通過獲取key的hashcode值,而hashmap中構成其陣列的預...

jdk1 7 hashMap原始碼學習

預設初始化長度 static final int default initial capacity 16 最大長度 static final int maximum capacity 1 30 預設載入因子 size capacity loadfactor static final float de...

JDK1 7的HashMap原始碼解讀

default initial capacity 初始化容量,中為1 4 即為16。為什麼要這樣寫呢?maximum capacity 最大容量,中衛1 30 即為2的30次冪。30次冪的原因是 改屬性為int型別,int型別最大為4個位元組,共32個二進位制位,理論上可以向左移動31次,即31次冪...