我是一名很普通的雙非大三學生,跟不少同窗同樣,有着一顆想進大廠的夢。接下來的幾個月內,我將堅持寫博客,輸出知識的同時鞏固本身的基礎,記錄本身的成長和鍛鍊本身,備戰2021暑期實習面試!奧利給!!java
Map這個你們庭真的是成員不少呢,咱們能夠簡單回憶一下有哪些,我這裏例舉幾個:HashMap、TreeMap、LikedHashMap、ConcurrentHashMap(線程安全)、WeekHashMap、HashTable。不記的話,能夠搜搜其餘文章回顧一下哦。本文只討論HashMapnode
HashMap基本是咱們在平常使用中頻率特別高的一個數據結構類型了,同時也是面試常常問到的,圍繞着HashMap能展開一系列問題,好比:面試
本文不對源碼作過深的討論,由於我以爲實習生應該還不須要了解的那麼透徹,咱們須要作的是知道這些東西,源碼什麼的,每一步怎麼作的,感興趣的同窗能夠本身看一下。算法
1.HashMap中常見的屬性api
//HashMap的 初始容量爲 16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
//最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
//默認的擴容因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//轉換紅黑樹的臨界值,當鏈表長度大於此值時,會把鏈表結構轉換爲紅黑樹結構
static final int TREEIFY_THRESHOLD = 8;
//轉換鏈表的臨界值,當元素小於此值時,會將紅黑樹結構轉換成鏈表結構
static final int UNTREEIFY_THRESHOLD = 6;
//當數組容量大於 64 時,鏈表纔會轉化成紅黑樹
static final int MIN_TREEIFY_CAPACITY = 64;
//記錄迭代過程當中 HashMap 結構是否發生變化,若是有變化,迭代時會 fail-fast
transient int modCount;
//HashMap 的實際大小,可能不許(由於當你拿到這個值的時候,可能又發生了變化)
transient int size;
//存放數據的數組
transient Node<K,V>[] table;
// 擴容的門檻,有兩種狀況
// 若是初始化時,給定數組大小的話,經過 tableSizeFor 方法計算,數組大小永遠接近於 2 的冪次方,好比你給定初始化大小 19,實際上初始化大小爲 32,爲 2 的 5 次方。
// 若是是經過 resize 方法進行擴容,大小 = 數組容量 * 0.75
int threshold;
/** HashMap中的內部類 **/
//鏈表的節點 1.7以前叫Entry,1.8以後叫Node
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
}
//紅黑樹的節點
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {}
複製代碼
上面已經對屬性寫了註釋,下面在補充一點:數組
什麼是擴容因子("加載因子,負載因子")? 擴容因子是用來判斷當前數組("哈希桶")何時須要進行擴容,假設因子爲0.5,那麼HashMap的初始化容量是16,則16*0.5 = 8個元素的時候,HashMap就會進行擴容。安全
爲何擴容因子是0.75? 這是均衡了時間和空間損耗算出來的值,由於當擴容因子設置比較大的時候,至關於擴容的門檻就變高了,發生擴容的頻率變低了,但此時發生Hash衝突的概率就會提高,當衝突的元素過多的時候,變成鏈表或者紅黑樹都會增長了查找成本(hash 衝突增長,鏈表長度變長)。而擴容因子太小的時候,會頻繁觸發擴容,佔用的空間變大,好比從新計算Hash等,使得操做性能會比較高。數據結構
HashMap初始化容量是多少? 在不指定capacity狀況下,初始化容量是16,但不是初始化的時候就建立了一個16大小的數組,而是在第一次put的時候去判斷是否須要初始化,我感受這有一點懶加載的味道。而且咱們常常在一些文章,包括阿里巴巴開發手冊中看到:"集合初始化時,指定集合初始值大小",若是有不少數據須要儲存到 HashMap 中,建議 HashMap 的容量一開始就設置成足夠的大小,這樣能夠防止在其過程當中不斷的擴容,影響性能,好比HashMap 須要放置1024 個元素,因爲沒有設置容量初始大小,隨着元素不斷增長,容量 7 次被迫擴大,resize 須要重建hash 表,嚴重影響性能。多線程
初始化容量爲何是16?爲何每次擴容是2的冪次方? 由於在使用是2的冪的數字的時候,Length-1的值是全部二進制位全爲1,這種狀況下,index的結果等同於HashCode後幾位的值。只要輸入的HashCode自己分佈均勻,Hash算法的結果就是均勻的。這是爲了實現均勻分佈。深刻解釋能夠看這篇文章:HashMap中hash(Object key)原理,爲何(hashcode >>> 16)。源碼分析
Node節點是什麼?。 node節點在1.7以前也叫entry節點,是HashMap存儲數據的一個節點,主要有四個屬性,hash,key,value,next,其實就是一個標準的鏈表節點,很容易理解。
鏈表何時會轉換成紅黑樹? 當鏈表長度大於等於 8 時,此時的鏈表就會轉化成紅黑樹,轉化的方法是:treeifyBin,此方法有一個判斷,當鏈表長度大於等於 8,而且整個數組大小大於 64 時,纔會轉成紅黑樹,當數組大小小於 64 時,只會觸發擴容,不會轉化成紅黑樹
加強 for 循環進行刪除,爲何會出現ConcurrentModificationException? 由於加強 for 循環過程其實調用的就是迭代器的 next () 方法,當你調用 map#remove () 方法進行刪除時,modCount 的值會 +1,而這時候迭代器中的 expectedModCount 的值卻沒有變,致使在迭代器下次執行 next () 方法時,expectedModCount != modCount 就會報 ConcurrentModificationException 的錯誤。這實際上是一種快速失敗的機制,java.util下面的集合類基本都是快速失敗的,實現都同樣,都是依靠這兩個變量。
可使用迭代器的remove()方法去刪除,由於 Iterator.remove () 方法在執行的過程當中,會把最新的modCount 賦值給 expectedModCount,這樣在下次循環過程當中,modCount 和 expectedModCount 二者就會相等。
從文章開頭貼出的代碼屬性中咱們能夠看出,1.8版本的HashMap的底層其實是一個數組+鏈表+紅黑樹的結構,可是在1.7的時候是沒有紅黑樹的,這正是1.8版本中對查詢的優化,咱們都知道鏈表的查詢時間複雜度是O(n)的,由於它須要一個一個去遍歷鏈表上全部的節點,因此當鏈表長度過長的時候,會嚴重影響 HashMap 的性能,而紅黑樹具備快速增刪改查的特色,這樣就能夠有效的解決鏈表過長時操做比較慢的問題。因而在1.8中引入了紅黑樹。
咱們來看1.8中HashMap的新增圖示,就不在曬那一大段的新增代碼了。
咱們在文字總結描述一下,新增大概的步驟以下:爲何須要從新計算Hash?
1.7中計算下標:
static int indexFor(int h, int length) {
return h & (length-1);
}
// n 表示數組的長度,i 爲數組索引下標
1.8中計算下標:tab[i = (n - 1) & hash])
// 1.8中的高位運算
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
複製代碼
在jdk1.7中有indexFor(int h, int length)方法。jdk1.8裏沒有1.8中用tab[(n - 1) & hash]代替但原理同樣。從上咱們能夠看出是由於長度擴大之後,Hash的規則也隨之改變。這在1.7和1.8中差異不大,可是1.7須要與新的數組長度進行從新hash運算,這個方式是相對耗性能的,而在1.8中對這一步進行了優化,採用高位運算取代了ReHash操做,其實這是一種規律,由於每次擴容,其實元素的新位置就是原位置+原數組長度,不懂的能夠看jdk8之HashMap resize方法詳解(深刻講解爲何1.8中擴容後的元素新位置爲原位置+原數組長度)
咱們都知道1.7以前,JDK是頭插法,考慮頭插法的緣由是不用遍歷鏈表,提升插入性能,但在JDK8已經改成尾插法了,不存在這個死循環問題,因此問題就出在頭插法這。 我以爲這篇文章在這寫的很清楚,想深刻了解一下產生死循環的過程,能夠看這篇文章:老生常談,HashMap的死循環。總結一下來講就是:Java7在多線程操做HashMap時可能引發死循環,緣由是擴容轉移後先後鏈表順序倒置,在轉移過程當中修改了原來鏈表中節點的引用關係。Java8在一樣的前提下並不會引發死循環,緣由是擴容轉移後先後鏈表順序不變,保持以前節點的引用關係。
那麼1.8以後,HashMap就是線程安全的了嘛? 首先咱們要知道所謂線程安全是對(讀/寫)兩種狀況都是數據一致而言的,而只讀且不變化的話,HashMap也是線程安全的,之因此不安全是在寫的時候,索引構建的時候會產生構建不一致的狀況,好比沒法保證上一秒put的值,下一秒get的時候仍是原值,因此線程安全仍是沒法保證,因此要問面試官有沒有讀寫並存的狀況。且java.util下面的集合類基本都不是線程安全的。因此HashMap 是非線程安全的,咱們能夠本身在外部加鎖,或者經過 Collections#synchronizedMap 來實現線程安全,Collections#synchronizedMap 的實現是在每一個方法上加上了 synchronized 鎖;或者使用ConcurrentHashMap
HashMap 的內容雖然較多,但大多數 api 都只是對數組 + 鏈表 + 紅黑樹這種數據結構進行封裝而已。那麼看完這些,你能觸類旁通答出一下問題了嗎?