1. 程式人生 > >java jdk7 hashMap實現原理

java jdk7 hashMap實現原理

在官方文件中是這樣描述HashMap的:

Hash table based implementation of the Map interface. This implementation provides all of the optional map operations, and permits null values and the null key. (The HashMap class is roughly equivalent to Hashtable, except that it is unsynchronized and permits nulls

.) This class makes no guarantees as to the order of the map; in particular, it does not guarantee that the order will remain constant over time.

幾個關鍵的資訊:基於Map介面實現、允許null鍵/值、非同步、不保證有序(比如插入的順序)、也不保證序不隨時間變化。

需要注意的是:Hashmap 不是同步的,如果多個執行緒同時訪問一個 HashMap,而其中至少一個執行緒從結構上(指新增或者刪除一個或多個對映關係的任何操作)修改了,則必須保持外部同步,以防止對對映進行意外的非同步訪問。

在 Java 程式語言中,最基本的結構就是兩種,一個是陣列,另外一個是指標(引用),HashMap 就是通過這兩個資料結構進行實現。HashMap實際上是一個“連結串列雜湊”的資料結構,即陣列和連結串列的結合體。HashMap 底層就是一個陣列結構,陣列中的每一項又是一個連結串列。當新建一個 HashMap 的時候,就會初始化一個數組。

在hashMap的建構函式中,建立了一個 Entry 的陣列,其大小為 capacity,Entry 是一個 static class,其中包含了 key 和 value,也就是鍵值對,另外還包含了一個 next 的 Entry 指標。Entry 就是陣列中的元素,每個 Entry 其實就是一個 key-value 對,它持有一個指向下一個元素的引用,這就構成了連結串列。

當我們往 HashMap 中 put 元素的時候,先根據 key 的 hashCode 重新計算 hash 值,根據 hash 值得到這個元素在陣列中的位置(即下標),如果陣列該位置上已經存放有其他元素了,那麼在這個位置上的元素將以連結串列的形式存放,新加入的放在鏈頭,最先加入的放在鏈尾。如果陣列該位置上沒有元素,就直接將該元素放到此陣列中的該位置上。當系統決定儲存 HashMap 中的 key-value 對時,完全沒有考慮 Entry 中的 value,僅僅只是根據 key 來計算並決定每個 Entry 的儲存位置。我們完全可以把 Map 集合中的 value 當成 key 的附屬,當系統決定了 key 的儲存位置之後,value 隨之儲存在那裡即可。

 

在 HashMap 中要找到某個元素,需要根據 key 的 hash 值來求得對應陣列中的位置。如何計算這個位置就是 hash 演算法。前面說過 HashMap 的資料結構是陣列和連結串列的結合,所以我們當然希望這個 HashMap 裡面的 元素位置儘量的分佈均勻些,儘量使得每個位置上的元素數量只有一個,那麼當我們用 hash 演算法求得這個位置的時候,馬上就可以知道對應位置的元素就是我們要的,而不用再去遍歷連結串列,這樣就大大優化了查詢的效率。

對於任意給定的物件,只要它的 hashCode() 返回值相同,那麼程式呼叫 hash(int h) 方法所計算得到的 hash 碼值總是相同的。我們首先想到的就是把 hash 值對陣列長度取模運算,這樣一來,元素的分佈相對來說是比較均勻的。但是,“模”運算的消耗還是比較大的,在 HashMap 中是這樣做的:呼叫 indexFor(int h, int length) 方法來計算該物件應該儲存在 table 陣列的哪個索引處。indexFor(int h, int length) 方法的程式碼如下:

/**
     * Returns index for hash code h.
     */
static int indexFor(int h, int length) {  
    return h & (length-1);
}

這個方法非常巧妙,它通過 h & (table.length -1) 來得到該物件的儲存位,而 HashMap 底層陣列的長度總是 2 的 n 次方,這是 HashMap 在速度上的優化。在 HashMap 構造器中有如下程式碼:

// Find a power of 2 >= initialCapacity
int capacity = 1;
    while (capacity < initialCapacity)  
        capacity <<= 1;

這段程式碼保證初始化時 HashMap 的容量總是 2 的 n 次方,即底層陣列的長度總是為 2 的 n 次方。

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

 

當 HashMap 中的元素越來越多的時候,hash 衝突的機率也就越來越高,因為陣列的長度是固定的。所以為了提高查詢的效率,就要對 HashMap 的陣列進行擴容,陣列擴容這個操作也會出現在 ArrayList 中,這是一個常用的操作,而在 HashMap 陣列擴容之後,最消耗效能的點就出現了:原陣列中的資料必須重新計算其在新陣列中的位置,並放進去,這就是 resize。

那麼 HashMap 什麼時候進行擴容呢?當 HashMap 中的元素個數超過陣列大小 *loadFactor時,就會進行陣列擴容,loadFactor的預設值為 0.75,這是一個折中的取值。也就是說,預設情況下,陣列大小為 16,那麼當 HashMap 中元素個數超過 16*0.75=12 的時候,就把陣列的大小擴充套件為 2*16=32,即擴大一倍,然後重新計算每個元素在陣列中的位置,而這是一個非常消耗效能的操作,所以如果我們已經預知 HashMap 中元素的個數,那麼預設元素的個數能夠有效的提高 HashMap 的效能。

HashMap 包含如下幾個構造器:

  • HashMap():構建一個初始容量為 16,負載因子為 0.75 的 HashMap。
  • HashMap(int initialCapacity):構建一個初始容量為 initialCapacity,負載因子為 0.75 的 HashMap。
  • HashMap(int initialCapacity, float loadFactor):以指定初始容量、指定的負載因子建立一個 HashMap。

負載因子 loadFactor 衡量的是一個散列表的空間的使用程度,負載因子越大表示散列表的裝填程度越高,反之愈小。對於使用連結串列法的散列表來說,查詢一個元素的平均時間是 O(1+a),因此如果負載因子越大,對空間的利用更充分,然而後果是查詢效率的降低;如果負載因子太小,那麼散列表的資料將過於稀疏,對空間造成嚴重浪費。