本文為個人學習筆記分享,沒有任何商業化行為,對其他文章的引用都會標記。如有侵權行為,請及時提醒更正!如需轉載請表明出處
部分參考資料
https://blog.csdn.net/j1231230/article/details/78072115
今天有幸去玩吧面試Android工程師一崗位,面試過程輕松愉快。在面試過程中被問到關于HashMap相關的問題。
1.HashMap的數據結構?
答:數組+鏈表。java8以后增加紅黑二叉樹,為了優化鏈表過長時的查找速度。(這里簡單的介紹一下紅黑二叉樹的特點,及優點。鏈表轉紅黑樹的時機就可以了)
2.HashMap是否線程安全?
答:不安全
3.如果想使用線程安全的HashMap該如何做?
答:1.Collections 工具類中 synchronizedCollection
2.采用裝飾者模式實現map接口對HashMap進行封裝。
3. ConcurrentHashMap 這個也是最推薦使用的線程安全的Map,也是實現方式最復雜的一個集合,每個版本的實現方式也不一樣,在jdk8之前是使用分段加鎖的一個方式,分成16個桶,每次只加鎖其中一個桶,而在jdk8又加入了紅黑樹和CAS算法來實現。
4.HashMap怎么通過key判斷索引位置的?
答:通過Hash算法,獲取key的Hash值,通過indexFor算法計算真實索引
// 獲取key對應的value
public V get(Object key) {
if (key == null)
return getForNullKey();
// 獲取key的hash值
int hash = hash(key.hashCode());
// 在“該hash值對應的鏈表”上查找“鍵值等于key”的元素
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;
}
indexFor算法的精髓
static int indexFor(int h, int length) {
return h & (length-1);
}
它沒有對hash表的長度取余而使用了位運算來得到索引,這是為什么呢,頓生懷疑~(PS:當時老朽就答的取余)
HashMap的初始容量和擴容都是以2的次方來進行的,那么length-1換算成二進制的話肯定所有位都為1,就比如2的3次方為8,length-1的二進制表示就是111, 而按位與計算的原則是兩位同時為“1”,結果才為“1”,否則為“0”。所以h& (length-1)運算從數值上來講其實等價于對length取模,也就是h%length。
如果不滿足前提條件“HashMap的初始容量和擴容都是以2的次方來進行的”,會發生什么問題呢?
假設當前table的length是15,二進制表示為1111,那么length-1就是1110,此時有兩個hash值為8和9的key需要計算索引值,計算過程如下:
8的二進制表示:1000
8&(length-1)= 1000 & 1110 = 1000,索引值即為8;
9的二進制表示:1001
9&(length-1)= 1001 & 1110 = 1000,索引值也為8;
這樣一來就產生了相同的索引值,也就是說兩個hash值為8和9的key會定位到數組中的同一個位置上形成鏈表,這就產生了碰撞。
而查詢的時候需要遍歷這個鏈表,這樣就降低了查詢的效率。同時,我們也可以發現,當數組長度為15的時候,hash值會與length-1(1110)進行按位與,那么最后一位永遠是0,而0001,0011,0101,1001,1011,0111,1101這幾個位置永遠都不能存放元素了,會造成嚴重的空間浪費,更糟的是這種情況下,數組可以使用的位置比數組長度小了很多,這意味著進一步增加了碰撞的幾率,減慢了查詢的效率。
因此可以看出,只有當數組長度為2的n次方時,不同的key計算得出的index索引相同的幾率才會較小,數據在數組上分布也比較均勻,碰撞的幾率也小,相對的,查詢的時候就不用遍歷某個位置上的鏈表,這樣查詢效率也就較高了。
此外,位運算快于十進制運算,hashmap擴容也是按位擴容,這樣同時也提高了運算效率。