HashMap使用經驗(下)

對於任意給定的對象,只要它的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();

}


感興趣的朋友可以掃二維碼關注公衆號——麥克叔叔每晚十點說,一起交流學習。

發佈了428 篇原創文章 · 獲贊 197 · 訪問量 82萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章