1. 程式人生 > >深入理解HashMap(原理,查詢,擴容)

深入理解HashMap(原理,查詢,擴容)

面試的時候聞到了Hashmap的擴容機制,之前只看到了Hasmap的實現機制,補一下基礎知識,講的非常好

原文連結:

Hashmap是一種非常常用的、應用廣泛的資料型別,最近研究到相關的內容,就正好複習一下。網上關於hashmap的文章很多,但到底是自己學習的總結,就發出來跟大家一起分享,一起討論。  1、hashmap的資料結構  要知道hashmap是什麼,首先要搞清楚它的資料結構,在java程式語言中,最基本的結構就是兩種,一個是陣列,另外一個是模擬指標(引用),所有的資料結構都可以用這兩個基本結構來構造的,hashmap也不例外。Hashmap實際上是一個數組和連結串列的結合體(在資料結構中,一般稱之為“連結串列雜湊“),請看下圖(橫排表示陣列,縱排表示陣列元素【實際上是一個連結串列】)。 

  從圖中我們可以看到一個hashmap就是一個數組結構,當新建一個hashmap的時候,就會初始化一個數組。我們來看看java程式碼:

/** 
     * The table, resized as necessary. Length MUST Always be a power of two. 
     *  FIXME 這裡需要注意這句話,至於原因後面會講到 
     */  
    transient Entry[] table;  
static class Entry<K,V> implements Map.Entry<K,V> {  
        final K key;  
        V value;  
        final int hash;  
        Entry<K,V> next;  
..........  
}  

上面的Entry就是陣列中的元素,它持有一個指向下一個元素的引用,這就構成了連結串列。       當我們往hashmap中put元素的時候,先根據key的hash值得到這個元素在陣列中的位置(即下標),然後就可以把這個元素放到對應的位置中了。如果這個元素所在的位子上已經存放有其他元素了,那麼在同一個位子上的元素將以連結串列的形式存放,新加入的放在鏈頭,最先加入的放在鏈尾。從hashmap中get元素時,首先計算key的hashcode,找到陣列中對應位置的某一元素,然後通過key的equals方法在對應位置的連結串列中找到需要的元素。從這裡我們可以想象得到,如果每個位置上的連結串列只有一個元素,那麼hashmap的get效率將是最高的,但是理想總是美好的,現實總是有困難需要我們去克服,哈哈~  2、hash演算法  我們可以看到在hashmap中要找到某個元素,需要根據key的hash值來求得對應陣列中的位置。如何計算這個位置就是hash演算法。前面說過hashmap的資料結構是陣列和連結串列的結合,所以我們當然希望這個hashmap裡面的元素位置儘量的分佈均勻些,儘量使得每個位置上的元素數量只有一個,那麼當我們用hash演算法求得這個位置的時候,馬上就可以知道對應位置的元素就是我們要的,而不用再去遍歷連結串列。  所以我們首先想到的就是把hashcode對陣列長度取模運算,這樣一來,元素的分佈相對來說是比較均勻的。但是,“模”運算的消耗還是比較大的,能不能找一種更快速,消耗更小的方式那?java中時這樣做的,

static int indexFor(int h, int length) {  
       return h & (length-1);  
   }  

首先算得key得hashcode值,然後跟陣列的長度-1做一次“與”運算(&)。看上去很簡單,其實比較有玄機。比如陣列的長度是2的4次方,那麼hashcode就會和2的4次方-1做“與”運算。很多人都有這個疑問,為什麼hashmap的陣列初始化大小都是2的次方大小時,hashmap的效率最高,我以2的4次方舉例,來解釋一下為什麼陣列大小為2的冪時hashmap訪問的效能最高。           看下圖,左邊兩組是陣列長度為16(2的4次方),右邊兩組是陣列長度為15。兩組的hashcode均為8和9,但是很明顯,當它們和1110“與”的時候,產生了相同的結果,也就是說它們會定位到陣列中的同一個位置上去,這就產生了碰撞,8和9會被放到同一個連結串列上,那麼查詢的時候就需要遍歷這個連結串列,得到8或者9,這樣就降低了查詢的效率。同時,我們也可以發現,當陣列長度為15的時候,hashcode的值會與14(1110)進行“與”,那麼最後一位永遠是0,而0001,0011,0101,1001,1011,0111,1101這幾個位置永遠都不能存放元素了,空間浪費相當大,更糟的是這種情況中,陣列可以使用的位置比陣列長度小了很多,這意味著進一步增加了碰撞的機率,減慢了查詢的效率!              所以說,當陣列長度為2的n次冪的時候,不同的key算得得index相同的機率較小,那麼資料在陣列上分佈就比較均勻,也就是說碰撞的機率小,相對的,查詢的時候就不用遍歷某個位置上的連結串列,這樣查詢效率也就較高了。            說到這裡,我們再回頭看一下hashmap中預設的陣列大小是多少,檢視原始碼可以得知是16,為什麼是16,而不是15,也不是20呢,看到上面annegu的解釋之後我們就清楚了吧,顯然是因為16是2的整數次冪的原因,在小資料量的情況下16比15和20更能減少key之間的碰撞,而加快查詢的效率。  所以,在儲存大容量資料的時候,最好預先指定hashmap的size為2的整數次冪次方。就算不指定的話,也會以大於且最接近指定值大小的2次冪來初始化的,程式碼如下(HashMap的構造方法中):

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

總結:          本文主要描述了HashMap的結構,和hashmap中hash函式的實現,以及該實現的特性,同時描述了hashmap中resize帶來效能消耗的根本原因,以及將普通的域模型物件作為key的基本要求。尤其是hash函式的實現,可以說是整個HashMap的精髓所在,只有真正理解了這個hash函式,才可以說對HashMap有了一定的理解。

3、hashmap的resize         當hashmap中的元素越來越多的時候,碰撞的機率也就越來越高(因為陣列的長度是固定的),所以為了提高查詢的效率,就要對hashmap的陣列進行擴容,陣列擴容這個操作也會出現在ArrayList中,所以這是一個通用的操作,很多人對它的效能表示過懷疑,不過想想我們的“均攤”原理,就釋然了,而在hashmap陣列擴容之後,最消耗效能的點就出現了:原陣列中的資料必須重新計算其在新陣列中的位置,並放進去,這就是resize。           那麼hashmap什麼時候進行擴容呢?當hashmap中的元素個數超過陣列大小*loadFactor時,就會進行陣列擴容,loadFactor的預設值為0.75,也就是說,預設情況下,陣列大小為16,那麼當hashmap中元素個數超過16*0.75=12的時候,就把陣列的大小擴充套件為2*16=32,即擴大一倍,然後重新計算每個元素在陣列中的位置,而這是一個非常消耗效能的操作,所以如果我們已經預知hashmap中元素的個數,那麼預設元素的個數能夠有效的提高hashmap的效能。比如說,我們有1000個元素new HashMap(1000), 但是理論上來講new HashMap(1024)更合適,不過上面annegu已經說過,即使是1000,hashmap也自動會將其設定為1024。 但是new HashMap(1024)還不是更合適的,因為0.75*1000 < 1000, 也就是說為了讓0.75 * size > 1000, 我們必須這樣new HashMap(2048)才最合適,既考慮了&的問題,也避免了resize的問題。  4、key的hashcode與equals方法改寫  在第一部分hashmap的資料結構中,annegu就寫了get方法的過程:首先計算key的hashcode,找到陣列中對應位置的某一元素,然後通過key的equals方法在對應位置的連結串列中找到需要的元素。所以,hashcode與equals方法對於找到對應元素是兩個關鍵方法。  Hashmap的key可以是任何型別的物件,例如User這種物件,為了保證兩個具有相同屬性的user的hashcode相同,我們就需要改寫hashcode方法,比方把hashcode值的計算與User物件的id關聯起來,那麼只要user物件擁有相同id,那麼他們的hashcode也能保持一致了,這樣就可以找到在hashmap陣列中的位置了。如果這個位置上有多個元素,還需要用key的equals方法在對應位置的連結串列中找到需要的元素,所以只改寫了hashcode方法是不夠的,equals方法也是需要改寫滴~當然啦,按正常思維邏輯,equals方法一般都會根據實際的業務內容來定義,例如根據user物件的id來判斷兩個user是否相等。 在改寫equals方法的時候,需要滿足以下三點:  (1) 自反性:就是說a.equals(a)必須為true。  (2) 對稱性:就是說a.equals(b)=true的話,b.equals(a)也必須為true。  (3) 傳遞性:就是說a.equals(b)=true,並且b.equals(c)=true的話,a.equals(c)也必須為true。  通過改寫key物件的equals和hashcode方法,我們可以將任意的業務物件作為map的key(前提是你確實有這樣的需要)。

總結:          本文主要描述了HashMap的結構,和hashmap中hash函式的實現,以及該實現的特性,同時描述了hashmap中resize帶來效能消耗的根本原因,以及將普通的域模型物件作為key的基本要求。尤其是hash函式的實現,可以說是整個HashMap的精髓所在,只有真正理解了這個hash函式,才可以說對HashMap有了一定的理解。