面試題之--HashMap原理
一,概念:
1,HashMap是基於雜湊表的Map介面的非同步實現,允許使用null值和null鍵。當即key為null的鍵值對,hash值為0,hashmap儲存的就是0。所以一個hashmap物件只會儲存一個key為null的鍵值對,因為它們的hash值都相同。
1.2,HashMap的key為null時,是在talbe[0]連結串列中查詢key為null的元素,如果找到,則將value重新賦值給這個元素的value,並返回原來的value。 如果上面for迴圈沒找到則將這個元素新增到talbe[0]連結串列的表頭。
1.3,HashMap儲存的是鍵值對,
// HashMap的put方法 public V put(K key, V value) { if (table == EMPTY_TABLE) { inflateTable(threshold); } if (key == null) // key為null呼叫putForNullKey(value) return putForNullKey(value); int hash = hash(key); int i = indexFor(hash, table.length); 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; } } modCount++; addEntry(hash, key, value, i); return null; }
private V putForNullKey(V value) { for (Entry<K,V> e = table[0]; e != null; e = e.next) { if (e.key == null) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } modCount++; addEntry(0, null, value, 0); return null; }
2,HashMap實際上是一個“連結串列雜湊”的資料結構,也就是陣列和連結串列的結合體。HashMap底層就是一個數組結構(又稱為雜湊桶,每個桶裡面放的是連結串列),陣列中的每一項又是一個連結串列,連結串列中的每個節點,就是雜湊表中的每個元素。當新建一個HashMap的時候,就會初始化一個數組。當然,這是jdk1.8之前的.Jdk1.8之後,增加了紅黑樹,HashMap是陣列+連結串列+紅黑樹實現的.
在每個陣列元素上都一個連結串列結構,當資料被Hash後,得到陣列下標,把資料放在對應下標元素的連結串列上。
2.1,陣列:儲存區間連續,佔用記憶體嚴重,定址容易,插入刪除困難,
2.2 連結串列:儲存區間離散,佔用記憶體比較寬鬆,定址困難,插入刪除容易;所以,Hashmap綜合應用了這兩種資料結構,實現了定址容易,插入刪除也容易。
2.3,紅黑樹:在擴容的時候,即使負載因子和Hash演算法設計的再合理,也免不了會出現鏈條過長的情況,這就會影響hashmap的效能.所以jdk1.8引入了紅黑樹,當連結串列長度超過8時,就轉為紅黑樹,紅黑樹的增刪改查較快,就提高了hashmap的效能.
紅黑樹是一種資料結構,它是近似平衡的二叉查詢樹,,它能夠確保任何一個節點的左右子樹的高度差不會超過二者中較低那個的一陪。具體來說,紅黑樹是滿足如下條件的二叉查詢樹(binary search tree)
3,當新建一個HashMap的時候,就會初始化一個Entry陣列。每個 Map.Entry 其實就是一個key-value鍵值對,它持有一個指向下一個元素的引用,就構成了連結串列。
4,當HashMap中的元素越來越多的時候,hash衝突的機率也就越來越高,因為陣列的長度是固定的。所以為了提高查詢的效率,就要對HashMap的陣列進行擴容,原陣列中的資料必須重新計算其在新陣列中的位置,並放進去,這就是resize(調整大小)。
5,採用fail-fast機制
我們知道java.util.HashMap不是執行緒安全的,因此如果在使用迭代器的過程中有其他執行緒修改了map,那麼將丟擲ConcurrentModificationException,這就是所謂fail-fast策略。
6,在多執行緒使用場景中,應該儘量避免使用執行緒不安全的HashMap,而使用執行緒安全的ConcurrentHashMap.
二,你知道HashMap的工作原理嗎?
簡單地說,HashMap 在底層將 key-value 當成一個整體進行處理,這個整體就是 Entry陣列,通過 Entry[] 陣列來儲存所有的 key-value 鍵值對,通過put()和get()方法儲存和獲取物件。
當我們將鍵值對(也就是entry物件)傳遞給put()方法時,會根據hash演算法找到bucket位置來儲存值物件,再根據equals()方法決定其在該陣列位置上的連結串列中的儲存位置;當呼叫get()方法時,也會根據hash演算法找到其在陣列中的bucket位置,再根據equals()方法從改位置上的連結串列中取出這個entry物件.
三,當兩個物件的hashcode相同會發生什麼?
1, hashcode相同,它們的bucket位置就相同,‘碰撞’就會發生。它們會儲存在同一個bucket位置的連結串列中。鍵物件的equals()方法用來找到鍵值對。
2.equal()相等的兩個物件他們的hashCode()肯定相等,也就是用equal()對比是絕對可靠。
3.hashCode()相等的兩個物件他們的equal()不一定相等,就是hashCode()不是絕對可靠。
hashCode是物件在記憶體地址通過hash演算法得到的雜湊碼,比較兩個物件是否相等:
4.首先比較hashcode ,如果hashcode相等則進一步比較equals,不相等則兩個物件肯定不相等;
5.hashset,hashmap,hashtable等等,比如hashset裡要求物件不能重複,則他內部必然要對新增進去的每個物件進行對比,而他的對比規則就是像上面說的那樣,先hashCode(),如果hashCode()相同,再用equal()驗證,如果hashCode()都不同,則肯定不同,這樣對比的效率就很高了。
四,如果兩個鍵的hashcode相同,你如何獲取值物件?
當我們呼叫get()方法,HashMap會使用鍵物件的hashcode找到bucket位置,然後獲取值物件.
五,如果HashMap的大小超過了負載因子(load factor)定義的容量,怎麼辦?
預設的負載因子大小為0.75(陣列大小為16),也就是說,當一個map填滿了75%的bucket時候,和其它集合類(如ArrayList等)一樣,將會建立原來HashMap大小的兩倍的bucket陣列,來重新調整map的大小,並將原來的物件放入新的bucket陣列中。這個過程叫作rehashing,因為它呼叫hash方法找到新的bucket位置。
六,你瞭解重新調整HashMap大小存在什麼問題嗎?
當多執行緒的情況下,可能產生條件競爭(race condition).
七,小結
1,擴容是一個特別耗效能的操作,所以當程式設計師在使用HashMap的時候,估算map的大小,初始化的時候給一個大致的數值,避免map進行頻繁的擴容。
2,負載因子是可以修改的,也可以大於1,但是建議不要輕易修改,除非情況非常特殊。
3,HashMap是執行緒不安全的,不要在併發的環境中同時操作HashMap,建議使用ConcurrentHashMap。
4,JDK1.8引入紅黑樹大程度優化了HashMap的效能。
5,table的索引在邏輯上叫做“桶”(bucket),它儲存了連結串列的第一個元素。
6,key的hashcode()方法用來找到Entry物件所在的桶。
7, 如果兩個key有相同的hash值,他們會被放在table陣列的同一個桶裡面。
8,key的equals()方法用來確保key的唯一性。
9,運算大多采用位運算,效率更高
借鑑:
https://blog.csdn.net/abcdad/article/details/64123291