從新詳盡的理解HasMap

關於hashCode

hashCode的存在主要是用於查找的快捷性,如Hashtable,HashMap等,hashCode是用來在散列存儲結構中肯定對象的存儲地址的.java

1.hashcode是用來查找的,若是你學過數據結構就應該知道,在查找和排序這一章有 例如內存中有這樣的位置 0 1 2 3 4 5 6 7 而我有個類,這個類有個字段叫ID,我要把這個類存放在以上8個位置之一,若是不用hashcode而任意存放,那麼當查找時就須要到這八個位置裏挨個去找,或者用二分法一類的算法。 但若是用hashcode那就會使效率提升不少。 咱們這個類中有個字段叫ID,那麼咱們就定義咱們的hashcode爲ID%8,而後把咱們的類存放在取得得餘數那個位置。好比咱們的ID爲9,9除8的餘數爲1,那麼咱們就把該類存在1這個位置,若是ID是13,求得的餘數是5,那麼咱們就把該類放在5這個位置。這樣,之後在查找該類時就能夠經過ID除 8求餘數直接找到存放的位置了。 2.可是若是兩個類有相同的hashcode怎麼辦那(咱們假設上面的類的ID不是惟一的),例如9除以8和17除以8的餘數都是1,那麼這是否是合法的,回答是:能夠這樣。那麼如何判斷呢?在這個時候就須要定義 equals了。 也就是說,咱們先經過 hashcode來判斷兩個類是否存放某個桶裏,但這個桶裏可能有不少類,那麼咱們就須要再經過 equals 來在這個桶裏找到咱們要的類。 那麼。重寫了equals(),爲何還要重寫hashCode()呢? 想一想,你要在一個桶裏找東西,你必須先要找到這個桶啊,你不經過重寫hashcode()來找到桶,光重寫equals()有什麼用啊面試

理解了hashCode咱們來理解HashMap

HashMap概述

HashMap是基於哈希表的Map接口的非同步實現。此實現提供全部可選的映射操做,並容許使用null值和null鍵。此類不保證映射的順序,特別是它不保證該順序恆久不變。 在java編程語言中,最基本的結構就是兩種,一個是數組,另一個是模擬指針(引用),全部的數據結構均可以用這兩個基本結構來構造的,HashMap也不例外。HashMap其實是一個「鏈表散列」的數據結構,即數組和鏈表的結合體。算法

內部存儲

HashMap的內部存儲是一個數組(bucket),數組的元素Node實現了是Map.Entry接口(hash, key, value, next),next非空時指向定位相同的另外一個Entry,如圖:編程

關於hashCode

hashCode的存在主要是用於查找的快捷性,如Hashtable,HashMap等,hashCode是用來在散列存儲結構中肯定對象的存儲地址的.數組

1.hashcode是用來查找的,若是你學過數據結構就應該知道,在查找和排序這一章有 例如內存中有這樣的位置 0 1 2 3 4 5 6 7 而我有個類,這個類有個字段叫ID,我要把這個類存放在以上8個位置之一,若是不用hashcode而任意存放,那麼當查找時就須要到這八個位置裏挨個去找,或者用二分法一類的算法。 但若是用hashcode那就會使效率提升不少。 咱們這個類中有個字段叫ID,那麼咱們就定義咱們的hashcode爲ID%8,而後把咱們的類存放在取得得餘數那個位置。好比咱們的ID爲9,9除8的餘數爲1,那麼咱們就把該類存在1這個位置,若是ID是13,求得的餘數是5,那麼咱們就把該類放在5這個位置。這樣,之後在查找該類時就能夠經過ID除 8求餘數直接找到存放的位置了。 2.可是若是兩個類有相同的hashcode怎麼辦那(咱們假設上面的類的ID不是惟一的),例如9除以8和17除以8的餘數都是1,那麼這是否是合法的,回答是:能夠這樣。那麼如何判斷呢?在這個時候就須要定義 equals了。 也就是說,咱們先經過 hashcode來判斷兩個類是否存放某個桶裏,但這個桶裏可能有不少類,那麼咱們就須要再經過 equals 來在這個桶裏找到咱們要的類。 那麼。重寫了equals(),爲何還要重寫hashCode()呢? 想一想,你要在一個桶裏找東西,你必須先要找到這個桶啊,你不經過重寫hashcode()來找到桶,光重寫equals()有什麼用啊安全

理解了hashCode咱們來理解HashMap

HashMap概述

HashMap是基於哈希表的Map接口的非同步實現。此實現提供全部可選的映射操做,並容許使用null值和null鍵。此類不保證映射的順序,特別是它不保證該順序恆久不變。 在java編程語言中,最基本的結構就是兩種,一個是數組,另一個是模擬指針(引用),全部的數據結構均可以用這兩個基本結構來構造的,HashMap也不例外。HashMap其實是一個「鏈表散列」的數據結構,即數組和鏈表的結合體。數據結構

內部存儲

HashMap的內部存儲是一個數組(bucket),數組的元素Node實現了是Map.Entry接口(hash, key, value, next),next非空時指向定位相同的另外一個Entry,如圖:併發

HashMap實現存儲和讀取

存儲
public V put(K key, V value) {
      // HashMap容許存放null鍵和null值。
     // 當key爲null時,調用putForNullKey方法,將value放置在數組第一個位置。
     if (key == null)
         return putForNullKey(value);
     // 根據key的keyCode從新計算hash值。
     int hash = hash(key.hashCode());
     // 搜索指定hash值在對應table中的索引。
     int i = indexFor(hash, table.length);
     // 若是 i 索引處的 Entry 不爲 null,經過循環不斷遍歷 e 元素的下一個元素。
     for (Entry<K,V> e = table[i]; e != null; e = e.next) {
         Object k;
         if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
             // 若是發現已有該鍵值,則存儲新的值,並返回原始值
             V oldValue = e.value;
             e.value = value;
             e.recordAccess(this);
             return oldValue;
         }
     }
     // 若是i索引處的Entry爲null,代表此處尚未Entry。
     modCount++;
     // 將key、value添加到i索引處。
     addEntry(hash, key, value, i);
     return null;
 }
複製代碼

根據hash值獲得這個元素在數組中的位置(即下標),若是數組該位置上已經存放有其餘元素了,那麼在這個位置上的元素將以鏈表的形式存放,新加入的放在鏈頭,最早加入的放在鏈尾。若是數組該位置上沒有元素,就直接將該元素放到此數組中的該位置上。 hash(int h)方法根據key的hashCode從新計算一次散列。此算法加入了高位計算,防止低位不變,高位變化時,形成的hash衝突。編程語言

static int hash(int h) {
     h ^= (h >>> 20) ^ (h >>> 12);
     return h ^ (h >>> 7) ^ (h >>> 4);
 }
複製代碼

HashMap中要找到某個元素,須要根據key的hash值來求得對應數組中的位置。如何計算這個位置就是hash算法。前面說過HashMap的數據結構是數組和鏈表的結合,因此咱們固然但願這個HashMap裏面的元素位置儘可能的分佈均勻些,儘可能使得每一個位置上的元素數量只有一個,那麼當咱們用hash算法求得這個位置的時候,立刻就能夠知道對應位置的元素就是咱們要的,而不用再去遍歷鏈表,這樣就大大優化了查詢的效率。高併發

根據上面 put 方法的源代碼能夠看出,當程序試圖將一個key-value對放入HashMap中時,程序首先根據該 key的 hashCode() 返回值決定該 Entry 的存儲位置:若是兩個 Entry 的 key 的 hashCode() 返回值相同,那它們的存儲位置相同。若是這兩個 Entry 的 key 經過 equals 比較返回 true,新添加 Entry 的 value 將覆蓋集合中原有 Entry的 value,但key不會覆蓋。若是這兩個 Entry 的 key 經過 equals 比較返回 false,新添加的 Entry 將與集合中原有 Entry 造成 Entry 鏈,並且新添加的 Entry 位於 Entry 鏈的頭部——具體說明繼續看 addEntry() 方法的說明。

具體如何根據hash計算下標呢,參見

JDK 源碼中 HashMap 的 hash 方法原理是什麼?

獲取
public V get(Object key) {
     if (key == null)
         return getForNullKey();
     int hash = hash(key.hashCode());
     for (Entry<K,V> e = table[indexFor(hash, table.length)];
         e != null;
         e = e.next) {
         Object k;
         if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
             return e.value;
     }
     return null;
 }
複製代碼

從HashMap中get元素時,首先計算key的hashCode,找到數組中對應位置的某一元素,而後經過key的equals方法在對應位置的鏈表中找到須要的元素。

HashMap的resize

當hashmap中的元素愈來愈多的時候,碰撞的概率也就愈來愈高(由於數組的長度是固定的),因此爲了提升查詢的效率,就要對hashmap的數組進行擴容,數組擴容這個操做也會出如今ArrayList中,因此這是一個通用的操做,不少人對它的性能表示過懷疑,不過想一想咱們的「均攤」原理,就釋然了,而在hashmap數組擴容以後,最消耗性能的點就出現了:原數組中的數據必須從新計算其在新數組中的位置,並放進去,這就是resize。 那麼hashmap何時進行擴容呢?當hashmap中的元素個數超過數組大小loadFactor時,就會進行數組擴容,loadFactor的默認值爲0.75,也就是說,默認狀況下,數組大小爲16,那麼當hashmap中元素個數超過160.75=12的時候,就把數組的大小擴展爲216=32,即擴大一倍,而後從新計算每一個元素在數組中的位置,而這是一個很是消耗性能的操做,因此若是咱們已經預知hashmap中元素的個數,那麼預設元素的個數可以有效的提升hashmap的性能。好比說,咱們有1000個元素new HashMap(1000), 可是理論上來說new HashMap(1024)更合適,不過上面annegu已經說過,即便是1000,hashmap也自動會將其設置爲1024。 可是new HashMap(1024)還不是更合適的,由於0.751000 < 1000, 也就是說爲了讓0.75 * size > 1000, 咱們必須這樣new HashMap(2048)才最合適,既考慮了&的問題,也避免了resize的問題。

1.8的優化

Java8作的改變: 1.HashMap是數組+鏈表+紅黑樹(JDK1.8增長了紅黑樹部分),當鏈表長度>=8時轉化爲紅黑樹 在JDK1.8版本中,對數據結構作了進一步的優化,引入了紅黑樹。而當鏈表長度太長(默認超過8)時,鏈表就轉換爲紅黑樹,利用紅黑樹快速增刪改查的特色提升HashMap的性能,其中會用到紅黑樹的插入、刪除、查找等算法。 2. java8 中對hashmap護容不是從新計算全部元素在數組的位置,而是咱們使用的是2次冪的擴展(指長度擴爲原來2倍),因此,元素的位置要麼是在原位置,要麼是在原位置再移動2次冪的位置在擴充HashMap的時候,不須要像JDK1.7的實現那樣從新計算hash,只須要看看原來的hash值新增的那個bit是1仍是0就行了,是0的話索引沒變,是1的話索引變成「原索引+oldCap"。

面試中一般被問到的。

HashMap高併發狀況下會出現什麼問題?

擴容問題 HashMap的存放自定義類時,須要實現自定義類的什麼方法?

hashCode和equals.經過hash(hashCode)而後模運算(實際上是與的位操做)定位在Entry數組中的下標,而後遍歷這以後的鏈表,經過equals比較有沒有相同的key,若是有直接覆蓋value,若是沒有就從新建立一一個Entry。

hashmap爲何能夠插入空值?

HashMap中添加key == null的Entry時會調用putForNullKey方法直接去遍歷table[0]Entry鏈表,尋找e.key == null的Entry或者沒有找到遍歷結束若是找到了e.key==null,就保存null值對應的原值oldValue,而後覆蓋原值,並返回oldValue若是在table[O]Entrty鏈表中沒有找到就調用addEntry方法添加一個key爲null的Entry

Hashmap 爲何線程不安全(hash 碰撞和擴容致使)

HashMap擴容的的時候可能會造成環形鏈表,形成死循環。

相關文章
相關標籤/搜索