對於任意給定的對象,只要它的hashCode()返回值相同,那麼程序調用hash(int h)方法所計算得到的Hash碼值總是相同的。接下來程序會調用indexFor(int h, int length)方法來計算該對象應該保存在table數組的哪個索引處。indexFor(inth, int length)方法的代碼如清單3-48所示。
代碼清單3-48 HashMap的indexFor方法源代碼
static int indexFor(int h, int length)
{
return h &(length-1);
}
這個方法非常巧妙,它總是通過h&(table.length-1)來得到該對象的保存位置,而HashMap底層數組的長度總是2的n次方,當length總是2的倍數時,h&(length-1)是一個非常巧妙的設計:假設 h=5,length=16,那麼h&length-1將得到5;如果h=6,length=16,那麼h&length-1將得到6,如果h=15,length=16,那麼h&length-1將得到15;但是當h=16時,length=16時,那麼h&length-1將得到0了;當h=17時,length=16時,那麼h&length-1是1,這樣保證計算得到的索引值總是位於table數組的索引之內。
根據上面3-46所示的put方法源代碼可以看出,當程序試圖將一個key-value對放入HashMap中時,程序首先根據該key的hashCode()返回值決定該Entry的存儲位置,如果兩個 Entry的key的hashCode()返回值相同,那它們的存儲位置相同。如果這兩個Entry的key通過 equals比較返回true,新添加Entry的Value將覆蓋集合中原有Entry的Value,但key不會被覆蓋。如果這兩個Entry的key通過equals比較返回false,新添加的Entry將與集合中原有Entry形成Entry鏈,而且新添加的Entry位於Entry鏈的頭部。
當向HashMap中添加key-value對,由其key的hashCode()返回值決定該key-value對(就是Entry對象)的存儲位置。當兩個Entry對象的key的hashCode()返回值相同時,將由key通過eqauls()比較值決定是採用覆蓋行爲(返回true),還是產生Entry鏈(返回false)。
清單3-46所示代碼中也調用了addEntry(hash,key, value, i);方法,addEntry是HashMap提供的一個包訪問權限的方法,該方法僅用於添加一個key-value 對。代碼如清單3-49所示。
代碼清單3-49 HashMap的addEntry方法源代碼
void addEntry(int hash, K key, V value, int bucketIndex)
{
// 獲取指定 bucketIndex 索引處的 Entry
// table是一個普通數組,每個數組都有一個固定的長度,這個數組的長度就是HashMap的容量。
Entry<K,V> e =table[bucketIndex]; //
// 將新創建的 Entry 放入 bucketIndex 索引處,並讓新的 Entry 指向原來的 Entry
table[bucketIndex] = newEntry<K,V>(hash, key, value, e);
// 如果 Map 中的 key-value 對的數量超過了極限
//Size變量用於保存該 HashMap 中所包含的 key-value 對的數量。
//threshold變量包含了HashMap能容納的key-value對的極限,它的值等於HashMap的容量乘以負載因子(load factor)。
//當size++>= threshold時,HashMap 會自動調用resize方法擴充HashMap的容量。每擴充一次,HashMap 的容量就增大一倍。
if (size++ >=threshold)
// 把 table 對象的長度擴充到 2 倍
resize(2 *table.length);
}
系統總是將新添加的Entry對象放入table數組的bucketIndex索引處,如果bucketIndex索引處已經有了一個Entry對象,那新添加的Entry對象指向原有的Entry對象(產生一個Entry鏈),如果bucketIndex索引處沒有Entry對象,那麼通過代碼Entry<K,V>e=table[bucketIndex];確保e變量是null,也就是新放入的Entry對象指向Null,也就是沒有產生Entry鏈。
當HashMap的每個bucket裏存儲的Entry只是單個Entry,也就是沒有通過指針產生Entry鏈時,此時的HashMap具有最好的性能。當程序通過key取出對應value時,系統只要先計算出該key的hashCode()返回值,再根據該hashCode返回值找出該key在table數組中的索引,然後取出該索引處的Entry,最後返回該key對應的value即可。HashMap類的get(K key)方法代碼如清單3-50所示。
代碼清單3-50 HashMap的get方法源代碼
public V get(Object key)
{
// 如果 key 是 null,調用getForNullKey 取出對應的 value
if (key == null)
returngetForNullKey();
// 根據該 key 的 hashCode 值計算它的 hash 碼
int hash =hash(key.hashCode());
// 直接取出 table 數組中指定索引處的值,
for (Entry<K,V> e =table[indexFor(hash, table.length)];
e != null;
// 搜索該 Entry 鏈的下一個 Entr
e = e.next) // ①
{
Object k;
// 如果該 Entry 的 key 與被搜索 key 相同
if (e.hash == hash&& ((k = e.key) == key
||key.equals(k)))
return e.value;
}
return null;
}
如果HashMap的每個bucket裏只有一個Entry時,HashMap可以根據索引、快速地取出該 bucket裏的Entry。在發生“Hash衝突”的情況下,單個bucket裏存儲的不是一個Entry,而是一個Entry鏈,系統只能必須按順序遍歷每個Entry,直到找到想搜索的Entry爲止——如果恰好要搜索的Entry位於該Entry鏈的最末端(該Entry是最早放入該bucket中),那系統必須循環到最後才能找到該元素。
歸納起來簡單地說,HashMap在底層將key-value當成一個整體進行處理,這個整體就是一個Entry對象。HashMap底層採用一個Entry[]數組來保存所有的key-value對,當需要存儲一個 Entry對象時,會根據Hash算法來決定其存儲位置;當需要取出一個Entry時,也會根據Hash算法找到其存儲位置,直接取出該Entry。
當創建HashMap時,有一個默認的負載因子(load factor),其默認值爲0.75,這是時間和空間成本上一種折衷,增大負載因子可以減少Hash表(就是那個Entry數組)所佔用的內存空間,但會增加查詢數據的時間開銷,而查詢是最頻繁的的操作(HashMap的get()與put()方法都要用到查詢);減小負載因子會提高數據查詢的性能,但會增加 Hash表所佔用的內存空間。
綜上所述,我們可以在創建HashMap時根據實際需要適當地調整load factor的值,如果程序比較關心空間開銷、內存比較緊張,可以適當地增加負載因子,如果程序比較關心時間開銷,內存比較寬裕則可以適當的減少負載因子。通常情況下,程序員無需改變負載因子的值。
如果開始就知道HashMap會保存多個key-value對,可以在創建時就使用較大的初始化容量,如果HashMap中Entry的數量一直不會超過極限容量(capacity* load factor),HashMap就無需調用resize()方法重新分配table數組,從而保證較好的性能。當然,開始就將初始容量設置太高可能會浪費空間(系統需要創建一個長度爲capacity的Entry數組),因此創建HashMap時初始化容量設置也需要小心對待。
從上面的源代碼分析可以得出,HashMap的高性能需要以下3點來提供保證。
(1)提供高效的Hash算法;
(2)提供高效的算法,保證Hash值到內存地址(數組索引)的映射速度;
(3)根據內存地址(數組索引)可以直接取得對應的值。
此外,能夠不用Map就不要用了吧,當我們想遍歷一個用鍵值對形式保存的Map時,下面兩種方式其實效率都不高,如清單3-51所示。
代碼清單3-51 map循環代碼
for (K key : map.keySet()) {
V value : map.get(key);
}
for (Entry<K, V> entry : map.entrySet()) {
K key = entry.getKey();
V value =entry.getValue();
}
感興趣的朋友可以掃二維碼關注公衆號——麥克叔叔每晚十點說,一起交流學習。