HashMap的學習筆記

1. 相關數據結構

  • HashMap概括的講就是數組+線性鏈表,首先回顧一下HashMap涉及到的三種數據結構。
  • 這裏寫圖片描述

    1. 數組:一組連續的內存存儲數據,根據下標的查找複雜度爲O(1),根據給定的值查找複雜度爲O(n)。(查找快,插入刪除慢)。
    2. 線性鏈表:如果能直接定位,新增和刪除只需要O(1)的複雜度,但是查找定位需要遍歷,平均複雜度爲O(logn)(查找慢,插入刪除快)。
    3. 哈希表:在哈希表中進行添加,刪除,查找等操作,性能十分之高,不考慮哈希衝突的情況下,僅需一次定位即可完成,時間複雜度爲O(1)。
  • 哈希表的主幹是數組,因爲根據哈希函數得出索引後能夠直接在數組上一次定位;但是如果發生哈希衝突(根據hash得到的地址已經被佔用),則需要另外的解決方法,解決哈希衝突的方法一般是開放地址法以及鏈地址法。開放地址法是指衝突之後繼續找下一塊可用地址,而鏈地址法則是引入鏈表作爲數組的子結構繼續存儲數據。


2. HashMap的結構

  • HashMap的主幹是一個Entry數組。Entry是HashMap的基本組成單元,每一個Entry包含一個key-value鍵值對。數組是HashMap的主體,鏈表則是主要爲了解決哈希衝突而存在的。
  • Entry是HashMap中的一個靜態內部類。代碼如下
static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;
        V value;
        Entry<K,V> next;//存儲指向下一個Entry的引用,單鏈表結構
        int hash;//對key的hashcode值進行hash運算後得到的值,存儲在Entry,避免重複計算

        /**
         * Creates new entry.
         */
        Entry(int h, K k, V v, Entry<K,V> n) {
            value = v;
            next = n;
            key = k;
            hash = h;
        }
  • 整個HashMap的結構如下:
    這裏寫圖片描述
  • 如果定位到的數組位置不含鏈表(當前entry的next指向null),那麼對於查找,添加等操作很快,僅需一次尋址即可;如果定位到的數組包含鏈表,對於添加操作,其時間複雜度依然爲O(1),因爲最新的Entry會插入鏈表頭部,只需要簡單改變引用鏈即可,而對於查找操作來講,此時就需要遍歷鏈表,然後通過key對象的equals方法逐一比對查找。所以,性能考慮,HashMap中的鏈表出現越少,性能纔會越好。

2. HashMap的構造器

  • HashMap的常規構造方法如下:
public HashMap(int initialCapacity, float loadFactor) {     //此處對傳入的初始容量進行校驗,最大不能超過MAXIMUM_CAPACITY = 1<<30(230)
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);

        this.loadFactor = loadFactor;
        threshold = initialCapacity;     
        init();//init方法在HashMap中沒有實際實現,不過在其子類如 linkedHashMap中就會有對應實現
    }
  • 結合HashMap的構造方法,我們可以看到有兩個參數可以影響HashMap的性能:初始容量(inital capacity,初始爲16)負載係數(load facto,初始爲0.75)。初始容量指定了初始table的大小,負載係數用來指定自動擴容的臨界值。當entry的數量超過capacity*load_factor時,容器將自動擴容並重新哈希。對於插入元素較多的場景,將初始容量設大可以減少重新哈希的次數。

3. HashMap的get(Object key)

  • get(Object key)方法根據指定的key值返回對應的value,該方法調用了getEntry(Object key)得到相應的entry,然後返回entry.getValue()。因此getEntry()是算法的核心。源碼如下:
final Entry<K,V> getEntry(Object key) {

        if (size == 0) {
            return null;
        }
        //通過key的hashcode值計算hash值
        int hash = (key == null) ? 0 : hash(key);
        //indexFor (hash&length-1) 獲取最終數組索引,然後遍歷鏈表,通過equals方法比對找出對應記錄
        for (Entry<K,V> e = table[indexFor(hash, table.length)];
             e != null;
             e = e.next) {
            Object k;
            if (e.hash == hash && 
                ((k = e.key) == key || (key != null && key.equals(k))))
                return e;
        }
        return null;
    }
  • 算法思想是首先通過hash()函數得到對應bucket的下標,然後依次遍歷衝突鏈表,通過key.equals(k)方法來判斷是否是要找的那個entry。
    這裏寫圖片描述

4. 關鍵:hashCode()與equals()

  • 將對向放入到HashMap或HashSet中時,有兩個方法需要特別關心:hashCode()和equals()。hashCode()方法決定了對象會被放到哪個bucket裏,當多個對象的哈希值衝突時,equals()方法決定了這些對象是否是“同一個對象”。所以,如果要將自定義的對象放入到HashMap或HashSet中,需要@Override hashCode()和equals()方法。

5. HashMap的put(Object key,Object value)

  • 當程序試圖將一個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 鏈的頭部(頭插法)。源碼如下:
public V put(K key, V value) {
        //如果table數組爲空數組{},進行數組填充(爲table分配實際內存空間),入參爲threshold,此時threshold爲initialCapacity 默認是1<<4(24=16)
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }
       //如果key爲null,存儲位置爲table[0]或table[0]的衝突鏈上
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key);//對key的hashcode進一步計算,確保散列均勻
        int i = indexFor(hash, table.length);//獲取在table中的實際位置
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
        //如果該對應數據已存在,執行覆蓋操作。用新value替換舊value,並返回舊value
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
        modCount++;//保證併發訪問時,若HashMap內部結構發生變化,快速響應失敗
        addEntry(hash, key, value, i);//新增一個entry
        return null;
    }

4. HashMap的擴容(resize)

  • 當HashMap中的元素越來越多的時候,hash衝突的機率也就越來越高,因爲數組的長度是固定的。所以爲了提高查詢的效率,就要對HashMap的數組進行擴容,數組擴容這個操作也會出現在ArrayList中,這是一個常用的操作,而在HashMap數組擴容之後,最消耗性能的點就出現了:原數組中的數據必須重新計算其在新數組中的位置,並放進去,這就是resize。
  • 那麼HashMap什麼時候進行擴容呢?當HashMap中的元素個數超過數組大小*loadFactor時,就會進行數組擴容,loadFactor的默認值爲0.75,這是一個折中的取值。也就是說,默認情況下,數組大小爲16,那麼當HashMap中元素個數超過16*0.75=12的時候,就把數組的大小擴展爲 2*16=32,即擴大一倍,然後重新計算每個元素在數組中的位置,而這是一個非常消耗性能的操作,所以如果我們已經預知HashMap中元素的個數,那麼預設元素的個數能夠有效的提高HashMap的性能。
void resize(int newCapacity) {
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }

        Entry[] newTable = new Entry[newCapacity];
        transfer(newTable, initHashSeedAsNeeded(newCapacity));
        //transfer方法逐個遍歷鏈表,重新計算索引位置,將老數組數據複製到新數組中去
        table = newTable;
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }

5. HashMap的性能參數

  • HashMap 包含如下幾個構造器:
    1. HashMap():構建一個初始容量爲 16,負載因子爲 0.75 的 HashMap
    2. HashMap(int initialCapacity):構建一個初始容量爲 initialCapacity,負載因子爲 0.75 的 HashMap。
    3. HashMap(int initialCapacity, float loadFactor):以指定初始容量、指定的負載因子創建一個 HashMap。
  • initialCapacity:HashMap的最大容量,即爲底層數組的長度。
  • loadFactor:負載因子loadFactor定義爲:散列表的實際元素數目(n)/ 散列表的容量(m)。
  • 負載因子衡量的是一個散列表的空間的使用程度,負載因子越大表示散列表的裝填程度越高,反之愈小。對於使用鏈表法的散列表來說,查找一個元素的平均時間是O(1+a),因此如果負載因子越大,對空間的利用更充分,然而後果是查找效率的降低;如果負載因子太小,那麼散列表的數據將過於稀疏,對空間造成嚴重浪費。
  • HashMap的實現中,通過threshold字段來判斷HashMap的最大容量:
    threshold = (int)(capacity * loadFactor)
    結合負載因子的定義公式可知,threshold就是在此loadFactor和capacity對應下允許的最大元素數目,超過這個數目就重新resize,以降低實際的負載因子。默認的的負載因子0.75是對空間和時間效率的一個平衡選擇。當容量超出此最大容量時, resize後的HashMap容量是原來容量的兩倍

6. HashMap的Fail-Fast機制

  • java.util.HashMap不是線程安全的,因此如果在使用迭代器的過程中有其他線程修改了map,那麼將拋出ConcurrentModificationException,這就是所謂fail-fast策略。
  • 這一策略在源碼中的實現是通過modCount域,modCount顧名思義就是修改次數,對HashMap內容的修改都將增加這個值,那麼在迭代器初始化過程中會將這個值賦給迭代器的expectedModCount。
HashIterator() {
    expectedModCount = modCount;
    if (size > 0) { // advance to first entry
    Entry[] t = table;
    while (index < t.length && (next = t[index++]) == null)
        ;
    }
}
  • 在迭代過程中,判斷modCount跟expectedModCount是否相等,如果不相等就表示已經有其他線程修改了Map (注意到modCount聲明爲volatile,保證線程之間修改的可見性。
final Entry<K,V> nextEntry() {   
    if (modCount != expectedModCount)   
        throw new ConcurrentModificationException();
  • 一般來說,存在非同步的併發修改時,快速失敗迭代器盡最大努力拋出 ConcurrentModificationException,迭代器的快速失敗行爲應該僅用於檢測程序錯誤。

7. HashMap和Hashtable

  • HashMap不是線程安全的;HashTable是線程安全的,其線程安全是通過Sychronize實現。
  • 由於上述原因,HashMap效率高於HashTable
  • HashMap的鍵可以爲null,HashTable不可以
  • 多線程環境下,通常也不是用HashTable,因爲效率低。HashMap配合Collections工具類使用實現線程安全。同時還有ConcurrentHashMap可以選擇,該類的線程安全是通過Lock的方式實現的,所以效率高於Hashtable。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章