BAT面試題
1.HashMap的什麼時候擴容,哪些操作會觸發
當向容器添加元素的時候,會判斷當前容器的元素個數,如果大於等於閾值,即當前數組的長度乘以加載因子的值的時候,就要自動擴容。默認容量爲16,擴容因子是0.75,閾值爲12。
有參構造方法和put、merge操作都會導致擴容。
2.HashMap push方法的執行過程?
最先判斷桶的長度是否爲0,爲0的話則需要先對桶進行初始化操作,接着,求出hashcode並通過擾動函數確定要put到哪個桶中,若桶中沒有元素直接插入,若有元素則判斷key是否相等,如果相等的話,那麼就將value改爲我們put的value值,若不等的話,那麼此時判斷該點是否爲樹節點,如果是的話,調用putreeval方法,以樹節點的方式插入,如果不是樹節點,那麼就遍歷鏈表,如果找到了key那麼修改value,沒找到新建節點插到鏈表尾部,最後判斷鏈表長度是否大於8 是否要進行樹化。
3.HashMap檢測到hash衝突後,將元素插入在鏈表的末尾還是開頭?
因爲JDK1.7是用單鏈表進行的縱向延伸,採用頭插法就是能夠提高插入的效率,但是也會容易出現逆序且環形鏈表死循環問題。但是在JDK1.8之後是因爲加入了紅黑樹使用尾插法,能夠避免出現逆序且鏈表死循環的問題。
4.1.8還採用了紅黑樹,講講紅黑樹的特性,爲什麼人家一定要用紅黑樹而不是AVL、B樹之類的?
在CurrentHashMap中是加鎖了的,實際上是讀寫鎖,如果寫衝突就會等待,如果插入時間過長必然等待時間更長,而紅黑樹相對AVL樹B樹的插入更快,AVL樹查詢確實更快一些,但是對於操作密集型,紅黑樹的旋轉更少,效率更高。
5.HashMap get方法的執行過程?
首先和put一樣,確定對應的key在哪一個桶中,如果桶容量爲0或者該桶內沒有元素直接返回空,反之會判斷該桶會檢查桶中第一個元素是否和要查的key相等,相等的話直接返回,不相等的話判斷該節點是否爲樹節點,是的話以樹節點方式遍歷整棵樹來查找,不是的話那就說明存儲結構是鏈表,以遍歷鏈表的方式查找。
源碼與其中的算法技巧
構造方法
public HashMap(int initialCapacity, float loadFactor) {
//當指定的 initialCapacity (初始容量) < 0 的時候會拋出 IllegalArgumentException 異常
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
//當指定的 initialCapacity (初始容量)= MAXIMUM_CAPACITY(最大容量) 的時候
if (initialCapacity > MAXIMUM_CAPACITY)
//初始容量就等於 MAXIMUM_CAPACITY (最大容量)
initialCapacity = MAXIMUM_CAPACITY;
//當 loadFactory(負載因子)< 0 ,或者不是數字的時候會拋出 IllegalArgumentException 異常
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
//tableSizeFor()的主要功能是返回一個比給定整數大且最接近2的冪次方整數
//比如我們給定的數是12,那麼tableSizeFor()會返回2的4次方,也就是16,因爲16是最接近12並且大於12的數
this.threshold = tableSizeFor(
initialCapacity);
}
執行順序註釋寫的很清楚了,但是有些同學對最後對 tableSizeFor 方法很有疑問,這是用來求傳入容量的最小2的冪次方整數的。
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
這是一系列的或操作,舉個例子
n-=1;// n=1000000(二進制)
...//16、8無變化
n|=n>>>4;//n=n|(n>>>4)=1000000|0000100=1000100
n|=n>>>2;//n=n|(n>>>2)=1000100|0010001=1010101
...
看出規律來了吧,右移多少位,就把最高位右邊的第x位設置爲1;第二次,就把兩個爲1的右邊xx位再設置爲1;第n次,就把上一步出現的1右邊xxxx位置爲1;
這樣執行完,原來是1000000,變成了1111111,最後加1,就變成2的整數次方數了。之所以先減一是因爲有可能本身就是最小2的冪次方整數。
2.Put方法
put方法的核心就是 putVal ,源碼和執行過程如下。
//實現 put 和相關方法
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
//如果table爲空或者長度爲0,則進行resize()(擴容)
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//確定插入table的位置,算法是上面提到的 (n - 1) & hash,在 n 爲 2 的時候,相當於取模操作
if ((p = tab[i = (n - 1) & hash]) == null)
//找到key值對應的位置並且是第一個,直接插入
tab[i] = newNode(hash, key, value, null);
//在table的 i 的位置發生碰撞,分兩種情況
//1、key值是一樣的,替換value值
//2、key值不一樣的
//而key值不一樣的有兩種處理方式:1、存儲在 i 的位置的鏈表 2、存儲在紅黑樹中
else {
Node<K,V> e; K k;
//第一個Node的hash值即爲要加入元素的hash
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
//如果不是TreeNode的話,即爲鏈表,然後遍歷鏈表
for (int binCount = 0; ; ++binCount) {
//鏈表的尾端也沒有找到key值相同的節點,則生成一個新的Node
//並且判斷鏈表的節點個數是不是到達轉換成紅黑樹的上界達到,則轉換成紅黑樹
if ((e = p.next) == null) {
//創建鏈表節點並插入尾部
p.next = newNode(hash, key, value, null);
//超過了鏈表的設置長度(默認爲8)則轉換爲紅黑樹
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
//如果e不爲空
if (e != null) { // existing mapping for key
//就替換就的值
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
爲了方便理解,配圖
putVal中有一段代碼提到了resize(),也就是擴容,我們來看下源碼
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
//判斷Node的長度,如果不爲零
if (oldCap > 0) {
//判斷當前Node的長度,如果當前長度超過 MAXIMUM_CAPACITY(最大容量值)
if (oldCap >= MAXIMUM_CAPACITY) {
//新增閥值爲 Integer.MAX_VALUE
threshold = Integer.MAX_VALUE;
return oldTab;
}
//如果小於這個 MAXIMUM_CAPACITY(最大容量值),並且大於 DEFAULT_INITIAL_CAPACITY (默認16)
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY)
//進行2倍擴容
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
//指定新增閥值
newCap = oldThr;
//如果數組爲空
else { // zero initial threshold signifies using defaults
//使用默認的加載因子(0.75)
newCap = DEFAULT_INITIAL_CAPACITY;
//新增的閥值也就爲 16 * 0.75 = 12
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
//按照給定的初始大小計算擴容後的新增閥值
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
//擴容後的新增閥值
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
//擴容後的Node數組
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
//如果數組不爲空,將原數組中的元素放入擴容後的數組中
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
//如果節點爲空,則直接計算在新數組中的位置,放入即可
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
//拆分樹節點
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
//如果節點不爲空,且爲單鏈表,則將原數組中單鏈表元素進行拆分
Node<K,V> loHead = null, loTail = null;//保存在原有索引的鏈表
Node<K,V> hiHead = null, hiTail = null;//保存在新索引的鏈表
Node<K,V> next;
do {
next = e.next;
//哈希值和原數組長度進行&操作,爲0則在原數組的索引位置
//非0則在原數組索引位置+原數組長度的新位置
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
爲什麼會hash衝突?
就是根據key即經過一個函數f(key)得到的結果的作爲地址去存放當前的key,value鍵值對(這個是hashmap的存值方式),但是卻發現算出來的地址上已經有數據。這就是所謂的hash衝突。
hash衝突的幾種情況:
1兩個節點的key值相同(hash值一定相同),導致衝突
2 兩個節點的key值不同,由於hash函數的侷限性導致hash值相同,導致衝突
3 兩個節點的key值不同,hash值不同,但hash值對數組長度取模後相同,導致衝突
如何解決hash衝突?解決hash衝突的方法主要有兩種,一種是開放尋址法,另一種是鏈表法 。
開放尋址法--線性探測
開放尋址法的原理很簡單,就是當一個Key通過hash函數獲得對應的數組下標已被佔用的時候,我們可以尋找下一個空檔位置
比如有個Entry6通過hash函數得到的下標爲2,但是該下標在數組中已經有了其它的元素,那麼就向後移動1位,看看數組下標爲3的位置是否有空位
但是下標爲3的數組也已經被佔用了,那麼久再向後移動1位,看看數組下標爲4的位置是否爲空
數組下標爲4的位置還沒有被佔用,所以可以把Entry6存入到數組下標爲4的位置。這就是開放尋址的基本思路,尋址的方式有很多種,這裏只是簡單的一個示例
鏈表法
鏈表法也正是被應用在了HashMap中,HashMap中數組的每一個元素不僅是一個Entry對象,還是一個鏈表的頭節點。每一個Entry對象通過next指針指向它的下一個Entry節點。當新來的Entry映射到與之衝突的數組位置時,只需要插入到對應的鏈表中即可
額…… 寫不完了 其他操作下篇繼續
◆ ◆ ◆ ◆ ◆
關注並後臺回覆 “面試” 或者 “視頻”,
即可免費獲取最新2019BAT
大廠面試題和大數據微服務視頻
您的分享和支持是我更新的動力