一、HashMap的數據結構
HashMap是存儲鍵值對的集合,當然HashMap也是線程不安全的,每一個鍵值對存儲在一個Node<K,V>(JDK1.7中是Entry<K,V>。HashMap的主幹是一個名爲table的Node數組。
每個鍵值對Key的hash值就對應數組的下標,當遇到hash衝突時,HashMap採用的策略是鏈地址法。在JDK1.7中通過鍵值對Entry<K,V>中的next屬性來把hash衝突的所有Entry連接起來,因此每次都要遍歷鏈表才能得到所要找的鍵值對,增刪改查操作的時間複雜度爲O(n)。而在JDK1.8中,當鏈表長度大於8時,會將鏈表轉化爲一棵紅黑樹,增刪改查操作的時間複雜度爲O(log(n))。
注意,HashMap運行Key爲null,並且存儲在table[0]的位置,table[0]的位置只能存儲一個Key爲null的鍵值對。(Hashtable不允許key爲null)。
二、源碼分析
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;//hash值
final K key;//鍵值
V value;
Node<K,V> next;
……
}
key和hash屬性爲final的原因:在Java中,如果一個對象的屬性值在業務邏輯上不需要改變,就將其聲明爲final,這樣保證了安全性。而且在這裏若是讓key或者hash發生改變,會導致該鍵值對無法被查找到。transient int size;//實際鍵值對個數
int threshold;//閾值,超過到這個值後就要進行擴容
transient int modCount;//修改計數器,用於快速失敗
final float loadFactor;//加載因子
注意threshold = table.length (默認值爲16)* loadFactor(默認值爲0.75)這兩個值可以在構造時自行輸入。length值需要自己根據業務需求輸入,輸入合適的值能顯著減少擴容次數 。而loadFactor值在一般情況下0.75都是有不錯的效率的。但是若是對時間效率要求很高,對空間效率要求很低,可以減小loadFactor值,反之增大loadFactor值,可以大於1。modCount用於記錄對象的改變結構的次數(不包含修改value的值的操作),這是用於在多線程情況下,當多個線程併發修改HashMap的結構時,多個線程都會去修改modCount這個成員變量,而每個線程內部維護着一個局部變量的修改技術器,當線程做完修改操作後發現成員變量的modCount與局部變量不一致時,就拋出ConcurrentModificationException,這是fail-fast機制,Java中如ArrayList等線程不安全的集合都有這個機制。static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);//第一步與第二步
}
第三步是取模定位,在JDK1.7中是一個indexFor函數,而JDK1.8取消了這個函數,但是JDK吧把這個index操作整合到put操作和get操作中去了,當然其代碼原理都是一樣的。index = (n - 1) & hash//在這裏等價於hash%n
這裏你一定想問,爲什麼這個位運算能夠與取模運算相等了,這裏就要歸功於HashMap對容量的一個巧妙設計。HashMap規定length一定是2^N,N可爲任意整數,如果自定義的length長度不爲2的整數次冪,那麼就會自動取成大於設定值的最接近的2^N的值。在這個前提下,我們再來看這個二進制與運算。如在n=16的情況下,15的二進制碼爲 1111,不管是什麼數,和11111做&操作,低五位不會變,而高位全部爲0,即與hash%n的結果是一樣的。而且在計算機中,位運算的效率是最高的,因此這樣會大大提升查詢效率。if (++size > threshold)//當完成put操作後,發現新的size大於了閾值
resize();
每次擴容爲之前的兩倍:newThr = oldThr << 1; // double threshold
在擴容之後就要把具體鍵值對搬遷到新的table數組中。(圖片來自網絡)