十個問題帶你了解和掌握Java HashMap
一、前言
本篇內容是源于 “ 由阿里巴巴Java開發規約HashMap條目引發的故事”,并在此基礎上加了自己的對HashMap更多的思考認識和整理。并且作為一名java開發工程師,應該是要了解和掌握的這些知識!
- 在《阿里巴巴java開發規約中》提到:
【推薦】集合初始化時,指定集合初始值大小。
說明:HashMap使用如下構造方法進行初始化,如果暫時無法確定集合大小,那么指定默認值(16)即可!
在進行本篇的閱讀之前,首先請你花三分鐘時間,思考面關于HashMap的十個問題,帶著問題去閱讀內容效果更好!
問題如下:
1.HashMap 是什么,實現原理?
2.HashMap 默認bucket(桶)數組多大?(上面已經給出),最大容量是多少?
3.如果new HashMap<>(19),bucket數組多大?
4.HashMap 什么時候開辟bucket數組占用內存?
5.HashMap 何時擴容?
6.為什么String, Interger這樣的包裝類類適合作為HashMap的key(鍵)呢?
7.如果用自定義對象當做hashmap的key進行存儲要注意什么?
8.當兩個對象的hashcode相同會發生什么(如何解決hash沖突)?如果兩個鍵的hashcode相同,你如何獲取值對象?
9.HashMap 和 ConcurrentHashMap的區別?
10.jdk1.7和jdk1.8中HashMap的實現有哪些區別?
二:HashMap相關知識的整理和簡單介紹
HashMap是基于哈希表的Map實現的,一個Key對應一個Value,允許使用null鍵和null值,不保證映射的順序,特別是它不保證該順序恒久不變!是非線程安全的的。
其中 “不保證映射的順序,特別是它不保證該順序恒久不變” 如何理解?
當哈希表中的條目數超出了當前容量與負載因子的乘積( Capacity * LoadFactor)時的時候,哈希表進行rehash操作(即重建內部數據結構),此時映射順序可能會被打亂!
1.HashMap 是什么,實現原理?
HashMap是一個存儲key和value的集合,一個key對應一個value,實現原理是使用hash算法通過對key進行hash后存儲哈希表(也稱為哈希數組)中,哈希表(哈希數組)的每個元素都是一個單鏈表的頭節點,鏈表是用來解決沖突的,如果不同的key映射到了數組的同一位置處,就將其放入單鏈表中。
如果容量不足(超過了閥值)時,同樣會自動增長
看下圖(jDK1.7):
其中哈希表(哈希數組)和 單鏈表的節點元素
2.HashMap 默認bucket(桶)數組多大?(上面已經給出),最大容量是多少?
// 默認的初始容量(容量為HashMap中槽的數目)是16,且實際容量必須是2的整數次冪。
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 Capacity
// 默認加載因子為0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f; LoadFactor
public HashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
// 最大容量(必須是2的冪且小于2的30次方,傳入容量過大將被這個值替換)
static final int MAXIMUM_CAPACITY = 1 << 30;
總結: 默認值初始值為16,最大值2 的30次方。
3.如果new HashMap<>(19),bucket數組多大?
HashMap 的 bucket 數組大小一定是2的冪,如果 new 的時候指定了容量且不是2的冪,
實際容量會是最接近(大于)指定容量的2的冪,比如 new HashMap<>(19),比19大且最接近的2的冪是32,實際容量就是32。
//jdk1.7
private void inflateTable(int toSize) {
// Find a power of 2 >= toSize, 2的冪 >= toSize
int capacity = roundUpToPowerOf2(toSize); //計算一定為2的冪
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
table = new Entry[capacity];
initHashSeedAsNeeded(capacity);
}
4.HashMap 什么時候開辟bucket數組占用內存?
HashMap 在 new 后并不會立即分配bucket數組,而是第一次 put 時初始化**使用resize() 函數進行分配。(類似 ArrayList 在第一次 add 時分配空間)
5.HashMap 何時擴容?
數據 put 后,如果數據量超過threshold( Capacity * LoadFactor ),就要resize!
//jdk1.7
void addEntry(int hash, K key, V value, int bucketIndex) {
//每次加入鍵值對時,都要判斷當前已用的size是否大于等于threshold(閥值),如果大于等于,則進行擴容,將容量擴為原來容量的2倍。
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
createEntry(hash, key, value, bucketIndex);
}
resize()方法進行擴容,擴容是一個相當耗時的操作,因為它需要重新計算這些元素在新的數組中的位置并進行復制處理。(具體可以看源碼,jdk1.8進行相應的優化)
在用HashMap的時,如果能提前預估下HashMap中元素的個數,這樣有助于提高HashMap的性能。
6.為什么String, Interger這樣的包裝類類適合作為HashMap的key(鍵)呢?
String, Interger這樣的wrapper類作為HashMap的鍵是再適合不過了,而且String最為常用。因為String是不可變的,也是final的,而且已經重寫了equals()和hashCode()方法了。
其他的wrapper類也有這個特點。不可變性是必要的,因為為了要計算hashCode(),就要防止鍵值改變,如果鍵值在放入時和獲取時返回不同的hashcode的話,那么就不能從HashMap中找到你想要的對象。不可變性還有其他的優點如線程安全。
如果你可以僅僅通過將某個field聲明成final就能保證hashCode是不變的,那么請這么做吧。因為獲取對象的時候要用到equals()和hashCode()方法,那么鍵對象正確的重寫這兩個方法是非常重要的。
如果兩個不相等的對象返回不同的hashcode的話,那么碰撞的幾率就會小些,這樣就能提高HashMap的性能。
7.如果用自定義對象當做hashmap的key進行存儲要注意什么?
這是問題6的延伸。如果一個自定義對象做為key,一定要注意對象的不可變性,否則可能導致存入Map中的數據無法取出,造成內存泄漏!
(1).要注意這個對象是否為可變對象。
(2).一定要重寫hashcode方法和equals方法,因為在HashMap的源代碼里面,是先比較HashCode是否相等,同時要滿足引用相等或者equals相等。
8.當兩個對象的hashcode相同會發生什么(如何解決hash沖突)?如果兩個鍵的hashcode相同,你如何獲取值對象?
兩個對象hashcode相同,它們在的哈希bucket中找到了相同位置,會發生“碰撞”。因為HashMap使用鏈表存儲對象,這個Entry(包含有鍵值對的Map.Entry對象)會存儲在鏈表中。可以參考問題1中的圖!
當我們調用get()方法,HashMap會使用key的hashcode找到bucket位置,然后發現兩個對象存儲在一個哈希bucket中,找到bucket位置之后,會調用key.equals()方法去找到鏈表中正確的節點,最終找到要找的值對象。
9.HashMap 和 ConcurrentHashMap的區別?
說簡單點就是HashMap是線程不安全的,單線程情況下使用;而ConcurrentHashMap是線程安全的,多線程使用!
可以使用 Collections.synchronizedMap(new HashMap<String, Integer>());將HashMap封裝成線程安全的,其內部實現原理是使用了關鍵字synchronized。
10.jdk1.7和jdk1.8中HashMap的實現有哪些區別?
jdk1.7和jdk1.8的區別還是很多,下面介紹兩個!
(1):存儲結構
如圖(jDK1.8)
jdk1.7 :static class Entry<K,V> implements Map.Entry<K,V> {
jdk1.8 :static class Node<K,V> implements Map.Entry<K,V> {
jdk7內部使用使用Entry<K,V>而jdk1.8內部使用Node<K,V>,都是實現Map.Entry<K,V> ,最主要的區別就是列表長度大于8時轉為紅黑樹!
在JDK1.7版本中.不管負載因子和Hash算法設計的再合理,也免不了會出現拉鏈(單鏈表)過長的情況,一旦出現拉鏈(單鏈表)過長,會嚴重影響HashMap的性能。
在JDK1.8版本中,對數據結構做了進一步的優化,引入了紅黑樹。而當鏈表長度太長(默認超過8)時,鏈表就轉換為紅黑樹,利用紅黑樹快速增刪改查的特點提高HashMap的性能,其中會用到紅黑樹的插入、刪除、查找等算法。本文不再對紅黑樹展開討論,
想了解更多紅黑樹數據結構的工作原理可以參考 紅黑樹數據結構的工作原理
總結:
JDK7 中的 HashMap 采用數組+鏈表的結構來存儲數據。
JDK8 中的 HashMap 采用數組+鏈表或紅黑樹的結構來存儲數據。
(2):一些操作方法的優化如resize
resize()用來第一次初始化,或者 put 之后數據超過了threshold(Capacity * LoadFactor)后擴容,這里具體不貼代碼了,大概說明一下!
jdk1.7 直接擴容兩倍,table.length * 2; 源碼中使用resize(2 * table.length);
jdk1.8 優化數組下標計算: index = (table.length - 1) & hash ,由于 table.length 也就是capacity 肯定是2的N次方,使用 & 位運算意味著只是多了最高位, 這樣就不用重新計算 index,元素要么在原位置,要么在原位置+ oldCapacity
如果上面內容哪里有問題歡迎指出!或者你對上面的內容有自己的認識和理解也歡迎評論,希望互相溝通,共同成長!謝謝!
三:參考的博文
由阿里巴巴Java開發規約HashMap條目引發的故事
java集合系列——Map之HashMap介紹(八)
HashMap的工作原理
http://blog.csdn.net/ns_code/article/details/36034955
Java8系列之重新認識HashMap
四:更多知識學習
最后在推廣一個我整理的java知識點,目錄如下!有興趣的可以點擊閱讀閱讀一下!
java的線程安全、單例模式、JVM內存結構等知識學習和整理
如果帥氣(美麗)、睿智(聰穎),和我一樣簡單善良的你看到本篇博文中存在問題,請指出,我虛心接受你讓我成長的批評,謝謝閱讀!
祝你今天開心愉快!
歡迎訪問我的csdn博客,我們一同成長!
不管做什么,只要堅持下去就會看到不一樣!在路上,不卑不亢!