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