花了三天時間來仔細閱讀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/