花了三天時間來仔細閱讀hashMap的原始碼,期間補了下不少資料結構的知識,刷了不少相關的面試題並進行了整理
1.談一下HashMap的特性?
1.HashMap儲存鍵值對實現快速存取,允許為null。key值不可重複,若key值重複則覆蓋。
2.非同步,執行緒不安全。
3.底層是hash表,不保證有序(比如插入的順序)
2.談一下HashMap的底層原理是什麼?
基於hashing的原理,jdk8後採用陣列+連結串列+紅黑樹的資料結構。我們通過put和get儲存和獲取物件。當我們給put()方法傳遞鍵和值時,先對鍵做一個hashCode()的計算來得到它在bucket陣列中的位置來儲存Entry物件。當獲取物件時,通過get獲取到bucket的位置,再通過鍵物件的equals()方法找到正確的鍵值對,然後在返回值物件。
3.談一下hashMap中put是如何實現的?
1.計算關於key的hashcode值(與Key.hashCode的高16位做異或運算)
2.如果雜湊表為空時,呼叫resize()初始化雜湊表
3.如果沒有發生碰撞,直接新增元素到雜湊表中去
4.如果發生了碰撞(hashCode值相同),進行三種判斷
4.1:若key地址相同或者equals後內容相同,則替換舊值
4.2:如果是紅黑樹結構,就呼叫樹的插入方法
4.3:連結串列結構,迴圈遍歷直到連結串列中某個節點為空,尾插法進行插入,插入之後判斷連結串列個數是否到達變成紅黑樹的闕值8;也可以遍歷到有節點與插入元素的雜湊值和內容相同,進行覆蓋。
5.如果桶滿了大於閥值,則resize進行擴容
4.談一下hashMap中什麼時候需要進行擴容,擴容resize()又是如何實現的?
呼叫場景:
1.初始化陣列table
2.當陣列table的size達到闕值時即++size > load factor * capacity 時,也是在putVal函式中
實現過程:(細講)
1.通過判斷舊陣列的容量是否大於0來判斷陣列是否初始化過
否:進行初始化
-
判斷是否呼叫無參構造器,
-
是:使用預設的大小和闕值
-
否:使用建構函式中初始化的容量,當然這個容量是經過tableSizefor計算後的2的次冪數
-
是,進行擴容,擴容成兩倍(小於最大值的情況下),之後在進行將元素重新進行與運算複製到新的雜湊表中
概括的講:擴容需要重新分配一個新陣列,新陣列是老陣列的2倍長,然後遍歷整個老結構,把所有的元素挨個重新hash分配到新結構中去。
PS:可見底層資料結構用到了陣列,到最後會因為容量問題都需要進行擴容操作
5.談一下hashMap中get是如何實現的?
對key的hashCode進行hashing,與運算計算下標獲取bucket位置,如果在桶的首位上就可以找到就直接返回,否則在樹中找或者連結串列中遍歷找,如果有hash衝突,則利用equals方法去遍歷連結串列查詢節點。
6.談一下HashMap中hash函式是怎麼實現的?還有哪些hash函式的實現方式?
對key的hashCode做hash操作,與高16位做異或運算
還有平方取中法,除留餘數法,偽隨機數法
7.為什麼不直接將key作為雜湊值而是與高16位做異或運算?
因為陣列位置的確定用的是與運算,僅僅最後四位有效,設計者將key的雜湊值與高16為做異或運算使得在做&運算確定陣列的插入位置時,此時的低位實際是高位與低位的結合,增加了隨機性,減少了雜湊碰撞的次數。
HashMap預設初始化長度為16,並且每次自動擴充套件或者是手動初始化容量時,必須是2的冪。
8.為什麼是16?為什麼必須是2的冪?如果輸入值不是2的冪比如10會怎麼樣?
https://blog.csdn.net/sidihuo/article/details/78489820
https://blog.csdn.net/eaphyy/article/details/84386313
1.為了資料的均勻分佈,減少雜湊碰撞。因為確定陣列位置是用的位運算,若資料不是2的次冪則會增加雜湊碰撞的次數和浪費陣列空間。(PS:其實若不考慮效率,求餘也可以就不用位運算了也不用長度必需為2的冪次)
2.輸入資料若不是2的冪,HashMap通過一通位移運算和或運算得到的肯定是2的冪次數,並且是離那個數最近的數字
9.談一下當兩個物件的hashCode相等時會怎麼樣?
會產生雜湊碰撞,若key值相同則替換舊值,不然連結到連結串列後面,連結串列長度超過闕值8就轉為紅黑樹儲存
10.如果兩個鍵的hashcode相同,你如何獲取值物件?
HashCode相同,通過equals比較內容獲取值物件
11."如果HashMap的大小超過了負載因子(load factor)定義的容量,怎麼辦?
超過闕值會進行擴容操作,概括的講就是擴容後的陣列大小是原陣列的2倍,將原來的元素重新hashing放入到新的雜湊表中去。
12.HashMap和HashTable的區別
相同點:都是儲存key-value鍵值對的
不同點:
-
HashMap允許Key-value為null,hashTable不允許;
-
hashMap沒有考慮同步,是執行緒不安全的。hashTable是執行緒安全的,給api套上了一層synchronized修飾;
-
HashMap繼承於AbstractMap類,hashTable繼承與Dictionary類。
-
迭代器(Iterator)。HashMap的迭代器(Iterator)是fail-fast迭代器,而Hashtable的enumerator迭代器不是fail-fast的。所以當有其它執行緒改變了HashMap的結構(增加或者移除元素),將會丟擲ConcurrentModificationException。
-
容量的初始值和增加方式都不一樣:HashMap預設的容量大小是16;增加容量時,每次將容量變為"原始容量x2"。Hashtable預設的容量大小是11;增加容量時,每次將容量變為"原始容量x2 + 1";
-
新增key-value時的hash值演算法不同:HashMap新增元素時,是使用自定義的雜湊演算法。Hashtable沒有自定義雜湊演算法,而直接採用的key的hashCode()。
13.請解釋一下HashMap的引數loadFactor,它的作用是什麼?
loadFactor表示HashMap的擁擠程度,影響hash操作到同一個陣列位置的概率。預設loadFactor等於0.75,當HashMap裡面容納的元素已經達到HashMap陣列長度的75%時,表示HashMap太擠了,需要擴容,在HashMap的構造器中可以定製loadFactor。
14.傳統hashMap的缺點(為什麼引入紅黑樹?):
JDK 1.8 以前 HashMap 的實現是 陣列+連結串列,即使雜湊函式取得再好,也很難達到元素百分百均勻分佈。當 HashMap 中有大量的元素都存放到同一個桶中時,這個桶下有一條長長的連結串列,這個時候 HashMap 就相當於一個單連結串列,假如單連結串列有 n 個元素,遍歷的時間複雜度就是 O(n),完全失去了它的優勢。針對這種情況,JDK 1.8 中引入了 紅黑樹(查詢時間複雜度為 O(logn))來優化這個問題。
15. 平時在使用HashMap時一般使用什麼型別的元素作為Key?
選擇Integer,String這種不可變的型別,像對String的一切操作都是新建一個String物件,對新的物件進行拼接分割等,這些類已經很規範的覆寫了hashCode()以及equals()方法。作為不可變類天生是執行緒安全的,
原始碼解析閱讀:
2. https://juejin.im/post/5ad40593f265da23750759ad
3. https://juejin.im/post/5afbff9451882542877353dd
4. https://blog.csdn.net/panweiwei1994/article/details/76555359#commentBox
5.
更多關於hashMap集合的面試題:
https://zhuanlan.zhihu.com/p/32355676
https://zhuanlan.zhihu.com/p/40760616
https://juejin.im/post/5a99544ef265da23a334ab6c
https://www.jianshu.com/p/7af5bb1b57e2
https://baiqiantao.github.io/Java/%E9%9B%86%E5%90%88/3AFbAb/