HashMap的存儲原理

  HashMap是java中至關重要的數據結構,使用HashMap的場景很是之多,所以,瞭解HashMap實現的過程和原理,是很是有必要的,在一些面試中也會常常被問到。好了,咱們趕忙來研究java內部是怎麼實現HashMap的吧!java

  首先,咱們都知道,數組的元素查找的效率是不錯的,可是涉及到插入操做和刪除操做,效率低下,由於可能會涉及到後續元素位置的遷移。而另一個數據結構鏈表則很好的解決了這個問題,插入和刪除操做都只須要改變節點的指針就行,可是鏈表的檢索的效率就很低了,試想一下,要檢索的元素在鏈表的末尾,而咱們只能從鏈表頭開始走完整個鏈表,才能檢索到這個元素。而Hash表能給咱們帶來高效查詢,插入和刪除。它是怎麼作到的呢?面試

  Hash表的實質是構造記錄的存儲位置和其對應的關鍵字之間的映射函數f,關於Hash函數的構造方法,主要有以下幾種:數組

    (1)直接定址法,取關鍵字的某個線性函數做爲Hash函數即Hash(key) = a*key+b。這種方法不多使用,雖然不會發生衝突,可是當key很是多的時候,整張Hash表也會很是大,畢竟是一一映射的。數據結構

    (2)平方取中法,將key的平方的中間幾位數做爲獲得的Hash地址。函數

    (3)除留餘數法,將key除以某個數,獲得的餘數做爲Hash地址。this

還有一些方法咱們在此就不說了。當多個關鍵字通過這個Hash函數的映射而獲得同一個值的時候,就發生了Hash衝突。解決Hash衝突主要有兩種方法:spa

    (1)開放定址法:指針

                            

             其中i=1,2,3。。。。,k(k<=m-1),H(key)爲哈希函數,m爲哈希表表長,di爲增量序列,可能有下列2種狀況:code

             當 di=1,2,3....,m-1時,稱線性探測在散列;對象

             當    時,稱爲二次探測再散列。

    (2)鏈地址法:

             即將全部關鍵字爲同義詞的記錄存儲在同一線性表中。假設某哈希函數產生的哈希地址在區間[0,m-1]上,則設立一個指針型向量 ChainHash[m];

             其每一個份量的初始狀態都是空指針。凡是哈希地址爲i的記錄都插入到頭指針爲ChainHash[i]的鏈表中。在列表中的插入位置能夠在表頭或表尾;也能夠在中間,以保持同義詞在同一線性表中按關鍵字有序。

             例如:已知一組關鍵字爲(19,14,23,01,68,20,84,27,55,11,10,79),則按哈希函數H(key)=key MOD 13 和鏈地址法處理衝突構造所得的哈希表,以下圖所示:

                           

Java中的HashMap的基本結構就如上圖所示,豎着看是一個數組,橫着看就是多個鏈表。當新建一個HashMap的時候,就初始化了一個數組:

1 /** 
2  * The table, resized as necessary. Length MUST Always be a power of two. 
3  */  
4 
5 transient Entry[] table;  

關於transient關鍵字,是爲了使其修飾的對象不參與序列化,也就是說,這個對象沒法被持久化,這裏用這個關鍵字是有緣由的,因爲HashCode()方法是一個本地方法(由java調用本地的外部函數執行),因此不一樣的虛擬機,對於相同的hashCode 產生的 Code 值多是不同的,若是你使用默認的序列化,那麼反序列化後,元素的位置和以前的是保持一致的,但是因爲 hashCode 的值不同了,那麼定位函數 indexOf()返回的元素下標就會不一樣,這樣不是咱們所想要的結果.舉個網上大神的例子:

        向HashMap存一個entry, key爲 字符串"STRING", 在第一個java程序裏, "STRING"的hashcode()爲1, 存入第1號bucket; 在第二個java程序裏, "STRING"的hashcode()有可能就是2, 存入第2號bucket. 若是用默認的串行化(Entry[] table不用transient), 那麼這個HashMap從第一個java程序裏經過串行化導入第二個java程序後, 其內存分佈是同樣的,那麼我取1號bucket能拿到「STRING」這個key,取2號bucket也能拿到相同的key,這是不合理的。

所以,HashMap這個entry數組是不能夠串行化的。所以,HashMap本身實現了readObject和writeObject,在其中它只保存了bucket size,entry count(這兩個其實不是必需的,但有助於提升效率)和全部的key/value(這個纔是必須的)。

這就是數組內的鏈表:

1 static class Node<K,V> implements Map.Entry<K,V> {
2         final int hash;
3         final K key;
4         V value;
5         Node<K,V> next;   //持有的一個指向下一個元素的引用,構成鏈表。 6        ....
7 }

 下面讓咱們來看看Hash在put新元素時所作的操做:

 1 public V put(K key, V value) {   // HashMap容許存放null鍵和null值, 當key爲null時,調用putForNullKey方法,將value放置在數組第一個位置。  
 2     if (key == null)  
 3         return putForNullKey(value);   //null key 存放的老是數組的第一個元素中  4         int hash = hash(key.hashCode());   // 根據key的HashCode從新計算hash值
 5     int i = indexFor(hash, table.length);  //經過hash值算出在對應table中的索引(下標)。 
 6     for (Entry<K,V> e = table[i]; e != null; e = e.next) {   // 若是 i 索引處的 Entry 不爲 null,經過循環不斷遍歷 e 元素的下一個元素  
 7         Object k;  
 8         if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {   //若是存在key相等的情形時,則用新的value值覆蓋老的value的值  9             V oldValue = e.value;  
10             e.value = value;  
11             e.recordAccess(this);  
12             return oldValue;  
13         }  
14     }  
15     modCount++;  // 若是i索引處的Entry爲null,代表此處尚未Entry。  
16     addEntry(hash, key, value, i);   // 將key、value添加到i索引處。
17     return null;  
18 }  

怎麼新加傳進來的entry呢:

1 void addEntry(int hash, K key, V value, int bucketIndex) {   // 獲取指定 bucketIndex 索引處的 Entry,bucketIndex能夠理解爲存放的table中的index,當這個bucketIndex相同時,就是發生了Hash衝突,
2     Entry<K,V> e = table[bucketIndex];  
3     table[bucketIndex] = new Entry<K,V>(hash, key, value, e);   // 將新建立的 Entry 放入 bucketIndex 索引處,並讓新的 Entry 指向原來的 Entry
4 if (size++ >= threshold) // 若是新加入後的大小超過了當前最大容量,則把 table 對象的長度擴充到原來的2倍。
5 resize(2 * table.length);
6 }

 原來新加的entry都是加在了鏈表的頭端。

在取Entry的時候就很是簡單了,若是key等於null,直接取數組的第一個元素,若是不是,先計算出key的hashcode找到下標,再用key的equals方法判斷是否相等,若是相等,則返回對應的entry,若是不相等,則返回null:

 1 public V get(Object key) {  
 2     if (key == null)  
 3         return getForNullKey();  
 4     int hash = hash(key.hashCode());  
 5     for (Entry<K,V> e = table[indexFor(hash, table.length)];  
 6         e != null;  
 7         e = e.next) {  
 8         Object k;  
 9         if (e.hash == hash && ((k = e.key) == key || key.equals(k)))  
10             return e.value;  
11     }  
12     return null;  
13 }  

 關於HashMap的存儲原理就說到這裏啦,有什麼錯誤,請歡迎指正。

參考資料:http://zhangshixi.iteye.com/blog/672697

相關文章
相關標籤/搜索