一、HashMap概述
二、HashMap的數據結構
三、HashMap源碼分析
1、關鍵屬性
2、構造方法
3、存儲數據
4、調整大小
5、數據讀取
6、HashMap的性能參數
7、Fail-Fast機制
一、HashMap概述
HashMap基於哈希表的 Map 接口的實現。此實現提供所有可選的映射操作,並允許使用 null 值和 null 鍵。(除了不同步和允許使用 null 之外,HashMap 類與 Hashtable 大致相同。)此類不保證映射的順序,特別是它不保證該順序恆久不變。
值得注意的是HashMap不是線程安全的,如果想要線程安全的HashMap,可以通過Collections類的靜態方法synchronizedMap獲得線程安全的HashMap。
Map map = Collections.synchronizedMap(new HashMap());
二、HashMap的數據結構
HashMap的底層主要是基於數組和鏈表來實現的,它之所以有相當快的查詢速度主要是因爲它是通過計算散列碼來決定存儲的位置。HashMap中主要是通過key的hashCode來計算hash值的,只要hashCode相同,計算出來的hash值就一樣。如果存儲的對象對多了,就有可能不同的對象所算出來的hash值是相同的,這就出現了所謂的hash衝突。學過數據結構的同學都知道,解決hash衝突的方法有很多,HashMap底層是通過鏈表來解決hash衝突的。
圖中,紫色部分即代表哈希表,也稱爲哈希數組,數組的每個元素都是一個單鏈表的頭節點,鏈表是用來解決衝突的,如果不同的key映射到了數組的同一位置處,就將其放入單鏈表中。
我們看看HashMap中Entry類的代碼:
複製代碼
/** Entry是單向鏈表。
* 它是 “HashMap鏈式存儲法”對應的鏈表。
*它實現了Map.Entry 接口,即實現getKey(), getValue(), setValue(V value), equals(Object o), hashCode()這些函數
**/
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
// 指向下一個節點
Entry<K,V> next;
final int hash;
// 構造函數。
// 輸入參數包括"哈希值(h)", "鍵(k)", "值(v)", "下一節點(n)"
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
public final K getKey() {
return key;
}
public final V getValue() {
return value;
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
// 判斷兩個Entry是否相等
// 若兩個Entry的“key”和“value”都相等,則返回true。
// 否則,返回false
public final boolean equals(Object o) {
if (!(o instanceof Map.Entry))
return false;
Map.Entry e = (Map.Entry)o;
Object k1 = getKey();
Object k2 = e.getKey();
if (k1 == k2 || (k1 != null && k1.equals(k2))) {
Object v1 = getValue();
Object v2 = e.getValue();
if (v1 == v2 || (v1 != null && v1.equals(v2)))
return true;
}
return false;
}
// 實現hashCode()
public final int hashCode() {
return (key==null ? 0 : key.hashCode()) ^
(value==null ? 0 : value.hashCode());
}
public final String toString() {
return getKey() + "=" + getValue();
}
// 當向HashMap中添加元素時,繪調用recordAccess()。
// 這裏不做任何處理
void recordAccess(HashMap<K,V> m) {
}
// 當從HashMap中刪除元素時,繪調用recordRemoval()。
// 這裏不做任何處理
void recordRemoval(HashMap<K,V> m) {
}
}
HashMap其實就是一個Entry數組,Entry對象中包含了鍵和值,其中next也是一個Entry對象,它就是用來處理hash衝突的,形成一個鏈表。
三、HashMap源碼分析
1、關鍵屬性
先看看HashMap類中的一些關鍵屬性:
復1 transient Entry[] table;//存儲元素的實體數組
2
3 transient int size;//存放元素的個數
4
5 int threshold; //臨界值 當實際大小超過臨界值時,會進行擴容threshold = 加載因子*容量
6
7 final float loadFactor; //加載因子
8
9 transient int modCount;//被修改的次數碼
其中loadFactor加載因子是表示Hsah表中元素的填滿的程度.
若:加載因子越大,填滿的元素越多,好處是,空間利用率高了,但:衝突的機會加大了.鏈表長度會越來越長,查找效率降低。
反之,加載因子越小,填滿的元素越少,好處是:衝突的機會減小了,但:空間浪費多了.表中的數據將過於稀疏(很多空間還沒用,就開始擴容了)
衝突的機會越大,則查找的成本越高.
因此,必須在 “衝突的機會”與”空間利用率”之間尋找一種平衡與折衷. 這種平衡與折衷本質上是數據結構中有名的”時-空”矛盾的平衡與折衷.
如果機器內存足夠,並且想要提高查詢速度的話可以將加載因子設置小一點;相反如果機器內存緊張,並且對查詢速度沒有什麼要求的話可以將加載因子設置大一點。不過一般我們都不用去設置它,讓它取默認值0.75就好了。
2、構造方法
下面看看HashMap的幾個構造方法:
復public HashMap(int initialCapacity, float loadFactor) {
2 //確保數字合法
3 if (initialCapacity < 0)
4 throw new IllegalArgumentException(“Illegal initial capacity: ” +
5 initialCapacity);
6 if (initialCapacity > MAXIMUM_CAPACITY)
7 initialCapacity = MAXIMUM_CAPACITY;
8 if (loadFactor <= 0 || Float.isNaN(loadFactor))
9 throw new IllegalArgumentException(“Illegal load factor: ” +
10 loadFactor);
11
12 // Find a power of 2 >= initialCapacity
13 int capacity = 1; //初始容量
14 while (capacity < initialCapacity) //確保容量爲2的n次冪,使capacity爲大於initialCapacity的最小的2的n次冪
15 capacity <<= 1;
16
17 this.loadFactor = loadFactor;
18 threshold = (int)(capacity * loadFactor);
19 table = new Entry[capacity];
20 init();
21 }
22
23 public HashMap(int initialCapacity) {
24 this(initialCapacity, DEFAULT_LOAD_FACTOR);
25 }
26
27 public HashMap() {
28 this.loadFactor = DEFAULT_LOAD_FACTOR;
29 threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);
30 table = new Entry[DEFAULT_INITIAL_CAPACITY];
31 init();
32 }碼
我們可以看到在構造HashMap的時候如果我們指定了加載因子和初始容量的話就調用第一個構造方法,否則的話就是用默認的。默認初始容量爲16,默認加載因子爲0.75。我們可以看到上面代碼中13-15行,這段代碼的作用是確保容量爲2的n次冪,使capacity爲大於initialCapacity的最小的2的n次冪,至於爲什麼要把容量設置爲2的n次冪,我們等下再看。
重點分析下HashMap中用的最多的兩個方法put和get
3、存儲數據
下面看看HashMap存儲數據的過程是怎樣的,首先看看HashMap的put方法:
復public V put(K key, V value) {
// 若“key爲null”,則將該鍵值對添加到table[0]中。
if (key == null)
return putForNullKey(value);
// 若“key不爲null”,則計算該key的哈希值,然後將其添加到該哈希值對應的鏈表中。
int hash = hash(key.hashCode());
//搜索指定hash值在對應table中的索引
int i = indexFor(hash, table.length);
// 循環遍歷Entry數組,若“該key”對應的鍵值對已經存在,則用新的value取代舊的value。然後退出!
for (Entry
1 void addEntry(int hash, K key, V value, int bucketIndex) {
2 Entry<K,V> e = table[bucketIndex]; //如果要加入的位置有值,將該位置原先的值設置爲新entry的next,也就是新entry鏈表的下一個節點
3 table[bucketIndex] = new Entry<>(hash, key, value, e);
4 if (size++ >= threshold) //如果大於臨界值就擴容
5 resize(2 * table.length); //以2的倍數擴容
6 }
參數bucketIndex就是indexFor函數計算出來的索引值,第2行代碼是取得數組中索引爲bucketIndex的Entry對象,第3行就是用hash、key、value構建一個新的Entry對象放到索引爲bucketIndex的位置,並且將該位置原先的對象設置爲新對象的next構成鏈表。
第4行和第5行就是判斷put後size是否達到了臨界值threshold,如果達到了臨界值就要進行擴容,HashMap擴容是擴爲原來的兩倍。
4、調整大小
resize()方法如下:
重新調整HashMap的大小,newCapacity是調整後的單位
複製代碼
1 void resize(int newCapacity) {
2 Entry[] oldTable = table;
3 int oldCapacity = oldTable.length;
4 if (oldCapacity == MAXIMUM_CAPACITY) {
5 threshold = Integer.MAX_VALUE;
6 return;
7 }
8
9 Entry[] newTable = new Entry[newCapacity];
10 transfer(newTable);//用來將原先table的元素全部移到newTable裏面
11 table = newTable; //再將newTable賦值給table
12 threshold = (int)(newCapacity * loadFactor);//重新計算臨界值
13 }
複製代碼
新建了一個HashMap的底層數組,上面代碼中第10行爲調用transfer方法,將HashMap的全部元素添加到新的HashMap中,並重新計算元素在新的數組中的索引位置
當HashMap中的元素越來越多的時候,hash衝突的機率也就越來越高,因爲數組的長度是固定的。所以爲了提高查詢的效率,就要對HashMap的數組進行擴容,數組擴容這個操作也會出現在ArrayList中,這是一個常用的操作,而在HashMap數組擴容之後,最消耗性能的點就出現了:原數組中的數據必須重新計算其在新數組中的位置,並放進去,這就是resize。
那麼HashMap什麼時候進行擴容呢?當HashMap中的元素個數超過數組大小*loadFactor時,就會進行數組擴容,loadFactor的默認值爲0.75,這是一個折中的取值。也就是說,默認情況下,數組大小爲16,那麼當HashMap中元素個數超過16*0.75=12的時候,就把數組的大小擴展爲 2*16=32,即擴大一倍,然後重新計算每個元素在數組中的位置,擴容是需要進行數組複製的,複製數組是非常消耗性能的操作,所以如果我們已經預知HashMap中元素的個數,那麼預設元素的個數能夠有效的提高HashMap的性能。
5、數據讀取
複製代碼
1.public V get(Object key) {
2. if (key == null)
3. return getForNullKey();
4. int hash = hash(key.hashCode());
5. for (Entry