作者:孤獨煙 出處: http://rjzheng.cnblogs.com/
文章由點及線再及面,寫的非常好。修改部分內容
參考資料2:美團技術團隊 https://tech.meituan.com/2016/06/24/java-hashmap.html
- (1) HashMap的實現原理
- (2) HashMap在什么條件下擴容?
- (3) 講講HashMap的get/put的過程?
- (4) 為什么HashMap的在鏈表元素數量超過8時改為紅黑樹?
- (5) HashMap的並發問題?
- (6) 你一般用什么作為HashMap的key?
(1) HashMap的實現原理
此題可以分為下面幾個小問題來問
• 看過HashMap源碼嗎,知道原理嗎?
• 為什么用數組+鏈表?
• hash沖突你還知道哪些解決辦法?
• 用LinkedList代替數組結構可以么?
• 了解TreeMap嗎?
看過HashMap源碼嗎,知道原理嗎?
HashMap采用Entry數組來存儲key-value對,每一個鍵值對組成了一個Entry實體,Entry類實際上是一個單向的鏈表結構,它具有Next指針,可以連接下一個Entry實體。 只是在JDK1.8中,鏈表長度大於8的時候,鏈表會轉成紅黑樹。
為什么用數組+鏈表?
數組是用來確定桶的位置,利用元素的key的hash值對數組長度取模得到. 鏈表是用來解決hash沖突問題,當出現hash值一樣的情形,就在數組上的對應位置形成一條鏈表。
ps: 這里的hash值並不是指hashcode,而是將hashcode高低十六位異或過的。至於為什么要這么做,繼續往下看。
hash沖突你還知道哪些解決辦法?
比較出名的有四種 (1)開放定址法 (2)鏈地址法 (3)再哈希法 (4)公共溢出區域法
HashMap中使用的是鏈地址法。
用LinkedList代替數組結構可以么?
當然是可以的,稍微說明一下,此題的意思是,源碼中是這樣的
Entry[] table = new Entry[capacity];
ps: Entry就是一個鏈表節點。 那下面這樣表示,是否可行?
List<Entry> table = new LinkedList<Entry>();
答案很明顯,是可以的。
既然是可以的,為什么HashMap不用LinkedList,而選用數組?
因為用數組效率最高!在HashMap中,定位桶的位置是利用元素的key的哈希值對數組長度取模得到。此時,我們已得到桶的位置。顯然數組的查找效率比LinkedList大。
那ArrayList,底層也是數組,查找也快啊,為啥不用ArrayList?
因為采用基本數組結構,擴容機制可以自己定義,HashMap中數組擴容剛好是2的次冪,在做取模運算的效率高。 而ArrayList的擴容機制是1.5倍擴容,那ArrayList為什么是1.5倍擴容這就不在本文說明了。
了解TreeMap嗎?
TreeMap 則是基於紅黑樹的一種提供順序訪問的 Map,和HashMap不同,它的get、put、remove之類操作都是O(logn)的復雜度,具體順序可以由指定的Comparator來決定,或者根據鍵的自然順序來判斷
了解CurrentHashMapp嗎
Java8 ConcurrentHashMap結構基本上和Java8的HashMap一樣,使用synchronized+CAS來保證線程安全性。
(2) HashMap在什么條件下擴容?
• HashMap在什么條件下擴容?
• 為什么擴容是2的n次冪?
• 為什么要先高16位異或低16位再取模運算?
HashMap在什么條件下擴容?
如果bucket滿了(超過load factor*current capacity),就要resize。 load factor為0.75,為了最大程度避免哈希沖突 current capacity為當前數組大小。
為什么擴容是2的次冪?
HashMap為了存取高效,要盡量較少碰撞,就是要盡量把數據分配均勻,每個鏈表長度大致相同,這個實現就在把數據存到哪個鏈表中的算法
這個算法實際就是取模,hash%length。 但是,大家都知道這種運算不如位移運算快。
因此,源碼中做了優化hash&(length-1)。 也就是說hash%length==hash&(length-1)
為什么要先高16位異或低16位再取模運算?
來看一下jdk1.8里的hash方法源碼。1.7的比較復雜,就不看了。
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
hashmap這么做,是為了降低hash沖突的幾率。
(3) 講講HashMap的get/put的過程?
• 知道hashmap中put元素的過程是什么樣嗎?
• 知道hashmap中get元素的過程是什么樣嗎?
• hash算法是干嘛的?還知道哪些hash算法?
• 說說String中hashcode的實現?(常考)
知道hashmap中put元素的過程是什么樣嗎?
對key的hashCode()做hash運算,計算index; 如果沒碰撞直接放到bucket里; 如果碰撞了,以鏈表的形式存在buckets后; 如果碰撞導致鏈表過長(大於等於TREEIFY_THRESHOLD),就把鏈表轉換成紅黑樹(JDK1.8中的改動); 如果節點已經存在就替換old value(保證key的唯一性) 如果bucket滿了(超過load factor*current capacity),就要resize。
知道hashmap中get元素的過程是什么樣嗎?
對key的hashCode()做hash運算,計算index; 如果在bucket里的第一個節點里直接命中,則直接返回; 如果有沖突,則通過key.equals(k)去查找對應的Entry;
• 若為樹,則在樹中通過key.equals(k)查找,O(logn);
• 若為鏈表,則在鏈表中通過key.equals(k)查找,O(n)。
hash算法是干嘛的?還知道哪些hash算法?
Hash函數是指把一個大范圍映射到一個小范圍。把大范圍映射到一個小范圍的目的往往是為了節省空間,使得數據容易保存。
比較出名的算法有MD4、MD5等
說說String中hashcode的實現?
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value;
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}
String類中的hashCode計算方法還是比較簡單的,就是以31為權,每一位為字符的ASCII值進行運算,用自然溢出來等效取模。
哈希計算公式可以計為s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
那為什么以31為質數呢?
主要是因為31是一個奇質數,所以31*i=32*i-i=(i<<5)-i,這種位移與減法結合的計算相比一般的運算快很多。
(4) 為什么HashMap的在鏈表元素數量超過8時改為紅黑樹?
• jdk1.8中hashmap與之前有哪些不同?
• 為什么在解決hash沖突的時候,不直接用紅黑樹?而選擇先用鏈表,再轉紅黑樹?
• 不用紅黑樹,用二叉查找樹可以么?
• 當鏈表轉為紅黑樹后,什么時候退化為鏈表?
jdk1.8中HashMap與之前有哪些不同?
• 由數組+鏈表的結構改為數組+鏈表+紅黑樹。
• 優化了高位運算的hash算法:h^(h>>>16)
• 擴容后,元素要么是在原位置,要么是在原位置再移動2次冪的位置,且鏈表順序不變。
最后一條是重點,因為最后一條的變動,HashMap在1.8中,不會在出現死循環問題。
為什么在解決hash沖突的時候,不直接用紅黑樹?而選擇先用鏈表,再轉紅黑樹?
因為紅黑樹需要進行左旋,右旋,變色這些操作來保持平衡,而單鏈表不需要。 當元素小於8個當時候,此時做查詢操作,鏈表結構已經能保證查詢性能。當元素大於8個的時候,此時需要紅黑樹來加快查詢速度,但是新增節點的效率變慢了。
不用紅黑樹,用二叉查找樹可以么?
二叉查找樹在特殊情況下會退化成一條線性結構
當鏈表轉為紅黑樹后,什么時候退化為鏈表?
為6的時候退轉為鏈表。中間有個差值7可以防止鏈表和樹之間頻繁的轉換。假設一下,如果設計成鏈表個數超過8則鏈表轉換成樹結構,鏈表個數小於8則樹結構轉換成鏈表,如果一個HashMap不停的插入、刪除元素,鏈表個數在8左右徘徊,就會頻繁的發生樹轉鏈表、鏈表轉樹,效率會很低。
(5) HashMap的並發問題?
• HashMap在並發編程環境下有什么問題(jdk1.8以后)?
• 如何解決這些問題?
HashMap在並發編程環境下有什么問題(jdk1.8以后)?
• 多線程put的時候可能導致元素丟失
• put非null元素后get出來的卻是null
如何解決這些問題?
使用ConcurrentHashmap,Hashtable等線程安全集合類。
https://www.jianshu.com/p/5dbaa6707017
https://www.cnblogs.com/jajian/p/10385377.html#autoid-1-7-0
(6) 你一般用什么作為HashMap的key?
• 健可以為Null值么?
• 一般用什么作為HashMap的key?
• 用可變類當HashMap的key有什么問題?
健可以為Null值么?
可以,key為null的時候,hash算法最后的值以0來計算,也就是放在數組的第一個位置。
具體看上文中的hash源碼
一般用什么作為HashMap的key?
一般用Integer、String這種不可變類當HashMap當key,而且String最為常用。
• (1) 因為字符串是不可變的,所以在它創建的時候hashcode就被緩存了,不需要重新計算。這就使得字符串很適合作為Map中的鍵,字符串的處理速度要快過其它的鍵對象。這就是HashMap中的鍵往往都使用字符串。
• (2) 因為獲取對象的時候要用到equals()和hashCode()方法,那么鍵對象正確的重寫這兩個方法是非常重要的,這些類已經很規范的覆寫了hashCode()以及equals()方法。
用可變類當HashMap的key有什么問題?
hashcode可能發生改變,導致put進去的值,無法get出,如下所示
HashMap<List<String>, Object> changeMap = new HashMap<>();
List<String> list = new ArrayList<>();
list.add("hello");
Object objectValue = new Object();
changeMap.put(list, objectValue);
System.out.println(changeMap.get(list));
list.add("hello world"); // hashcode發生了改變
System.out.println(changeMap.get(list));
輸出結果如下
java.lang.Object@33909752
null
實現一個自定義的class作為HashMap的key該如何實現?
此題考察兩個知識點,這里就不展開了
• 重寫hashcode和equals方法需要注意什么
• 如何設計一個不變類