通過幾個問題來學習HashMap
前提大家都知道,HashMap是由哈希表實現的,哈希表就是由數組和鏈表組成的。
給出一個很形象的數據結構圖。
問題1.既然HashMap是數組+鏈表實現的,數組開始的時候一定是有一個固定長度的,那HashMap中的數組默認長度是多少呢?
默認情況下,內部數組的長度就是16,這個可以從HashMap的底層源碼的構造函數中看到。下面我們就開始HashMap的源碼閱讀之旅。
HashMap的構造函數有三個。默認我們都是不會傳這個initialCapacity的,如果不傳的話,那么就會使用默認為4的DEFAULT_INITIAL_CAPACITY
然后將initialCapacity的值賦給了threshold,我們會在put方法中判斷,如果是第一次put數據就會初始化Table,也就使用到了這個threshold。
那么是怎么初始化的呢?就是拿到這個threshold對其做一下平方。(roundUpToPowerOf2就是對2的冪次冪)
一步步追蹤源代碼,發現最后是返回的默認的4的平方的數組長度。
問題2.HashMap既然底層是一個線性的數組,那么是怎么實現的隨機存取呢?
(因為是隨機存取,所以是有些索引位置是沒有元素的,會產生一些空間的浪費,但是這其實就是空間換時間,讓HashMap既有數組的查詢快,又有鏈表的增刪快的優點)
// 存儲時:
int hash = key.hashCode();
int index = hash % Entry[].length;
Entry[index] = value;
// 取值時:
int hash = key.hashCode();
int index = hash % Entry[].length;
return Entry[index];
存儲的時候就是拿到key的hash值然后對HashMap底層數組的長度取余,取余的結果就是存儲的索引。
取值的時候一樣是拿到key的hash值然后對HashMap底層數組的長度取余,得到索引直接去數組里面取就行了。取出來是一個鏈表的封裝類Entry,然后遍歷一下Entry中的值取出我們要的就行了。
總結:元素存儲的規則 hash(key)%len, 就是key的hash值對HashMap底層數組的長度取余,這個公式一定要記住。
問題3.通過上面的問題我們了解了HashMap的存取,但是我們要知道Hash算法其實就是把任意長度的輸入變換成固定長度的輸出,這種轉換是一種壓縮映射,也就是說,Hash值的空間遠小于輸入的空間,不同的輸入可能會Hash成相同的輸出。而且我們又對HashMap默認size 16的數組長度取余,所以不同的key就更是有很大概率返回相同的索引了,那會不會就把之前存的數據給覆蓋了呢?
答案當然是否定的。不然誰還敢用HashMap存數據呢。
HashMap我們知道是數組+鏈表實現的,前面我們是只看到了數組,鏈表呢就是在這使用的。這里HashMap里面用到鏈式數據結構的一個概念。上面我們提到過Entry類里面有一個next屬性,作用是指向下一個Entry。打個比方, 第一個鍵值對A進來,通過計算其key的hash得到的index=0,記做:Entry[0] = A。一會后又進來一個鍵值對B,通過計算其index也等于0,現在怎么辦?HashMap會這樣做:B.next = A,Entry[0] = B,如果又進來C,index也等于0,那么C.next = B,Entry[0] = C;這樣我們發現index=0的地方其實存取了A,B,C三個鍵值對,他們通過next這個屬性鏈接在一起。也就是說數組中存儲的是最后插入的元素。(數組中只會存一個Entry元素,這一個元素的next就會指向下一個元素,這樣循環)
問題4.我們前面看到數組的默認長度是16,可以說很小,那他會不會擴容呢?
答案是肯定的。默認HashMap內部數組的長度為16,負載因子為0.75,就是在構造函數里面傳的兩個值。閾值就是12(16*0.75=12),這樣當第十三個元素加入時,底層數組就會擴容。擴容為原數組大小的兩倍。
resize(2*table.length)就是擴容的操作。擴容為原數組的兩倍。