HashMap的存儲原理


  HashMap是java中相當重要的數據結構,使用HashMap的場景非常之多,因此,了解HashMap實現的過程和原理,是非常有必要的,在一些面試中也會經常被問到。好了,我們趕緊來研究java內部是怎么實現HashMap的吧!

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

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

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

    (2)平方取中法,將key的平方的中間幾位數作為得到的Hash地址。

    (3)除留余數法,將key除以某個數,得到的余數作為Hash地址。

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

    (1)開放定址法:

                            

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

             當 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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM