HashMap原理知識點速查

數據結構之哈希表

  • 在哈希表中進行添加,刪除,查找等操作,性能十分之高,不考慮哈希衝突的情況下,僅需一次定位即可完成,時間複雜度爲O(1)

  • 數據結構的物理存儲結構只有兩種:

    • 順序存儲結構
    • 鏈式存儲結構
  • 哈希表的主幹就是數組。對於數組通過指定下標的查找,時間複雜度爲O(1)

  • 查找的本質:存儲位置 = f(關鍵字),f是一個哈希函數


    image
  • 哈希衝突:但是,鍵是可能存在衝突的,相當於不同的鍵得出了相同的哈希值。HashMap即是採用了鏈地址法,也就是數組+鏈表的方式。

HashMap的結構

  • HashMap的主幹是一個Entry數組。
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
  • 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,避免重複計算
...
}
  • 整體結構如下


    image
  • 解決的問題:哈希衝突
    • 如果定位到的數組位置不含鏈表,即當前entry的next指向null,則一次查詢即可。
    • 如果定位到的數組包含鏈表
      • 添加:O(1),直接插入鏈表頭部
      • 查找:O(n),遍歷鏈表,key對象的equals方法逐一比對查找

HashMap的源碼分析:插入

public V put(K key, V value) {
        //其允許存放null的key和null的value,放在table[0]
        if (key == null)
            return putForNullKey(value);
       
        int hash = hash(key);
        //得到鍵的哈希值,用來獲取數組中的索引
        int i = indexFor(hash, table.length);
        //如果i處的Entry不爲null,則需要在鏈表中添加,但是添加前需要看是否已存在,存在返回舊值,不存在則最終addEntry。
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            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++;
        addEntry(hash, key, value, i);
        return null;
}
void addEntry(int hash, K key, V value, int bucketIndex) {
        //添加前看是否需要擴容
        if ((size >= threshold) && (null != table[bucketIndex])) {
            resize(2 * table.length);
            hash = (null != key) ? hash(key) : 0;
            bucketIndex = indexFor(hash, table.length);
        }

        createEntry(hash, key, value, bucketIndex);
}

void createEntry(int hash, K key, V value, int bucketIndex) {
        // 獲取指定 bucketIndex 索引處的 Entry
        Entry<K,V> e = table[bucketIndex];
        // 將新創建的 Entry 放入 bucketIndex 索引處,並讓新的 Entry 指向原來的 Entr
        table[bucketIndex] = new Entry<>(hash, key, value, e);
        size++;
}

HashMap的源碼分析:讀取

    public V get(Object key) {
        if (key == null)
            return getForNullKey();
        Entry<K,V> entry = getEntry(key);

        return null == entry ? null : entry.getValue();
    }
    final Entry<K,V> getEntry(Object key) {
        int hash = (key == null) ? 0 : hash(key);
        //通過哈希得到的index的e不爲空則繼續搜索鏈表
        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;
    }

HashMap的性能參數

  • initialCapacity初始容量
  • transient int size; 實際存儲的key-value鍵值對的個數
  • int threshold; 最大容量,threshold一般爲 capacity*loadFactory,HashMap在進行擴容時需要參考threshold。初始容量默認爲16
  • final float loadFactor; 負載因子,代表了table的填充度有多少,默認是0.75,因此如果負載因子越大,對空間的利用更充分,然而後果是查找效率的降低;如果負載因子太小,那麼散列表的數據將過於稀疏,對空間造成嚴重浪費。
  • transient int modCount; 用於防止多線程問題的快速失敗。由於HashMap非線程安全,在對HashMap進行迭代時,如果期間其他線程的參與導致HashMap的結構發生變化了(比如put,remove等操作),在迭代過程中,判斷modCount跟expectedModCount是否相等,如果不相等就表示已經有其他線程修改了Map,則需要拋出異常ConcurrentModificationException

HashMap的擴容

  • HashMap數組的大小需要擴容時,原數組中的數據必須重新計算其在新數組中的位置,並放進去,這就是resize。
  • loadFactor的默認值爲0.75。默認情況下,數組大小爲16,那麼當HashMap中元素個數超過160.75=12的時候,就把數組的大小擴展爲 2*16=32,即擴大一倍,然後重新計算每個元素在數組中的位置。
  • 這是一個非常消耗性能的操作,可以預設一個大小。

參考

  1. Java集合學習1:HashMap的實現原理,李大輝,http://tracylihui.github.io/2015/07/01/Java%E9%9B%86%E5%90%88%E5%AD%A6%E4%B9%A01%EF%BC%9AHashMap%E7%9A%84%E5%AE%9E%E7%8E%B0%E5%8E%9F%E7%90%86/
  2. HashMap實現原理及源碼分析,dreamcatcher-cx,http://www.cnblogs.com/chengxiao/p/6059914.html

關於我:

linxinzhe,全棧工程師,目前供職於某世界500強銀行的金融科技部門(人工智能,區塊鏈)。

GitHub:https://github.com/linxinzhe

歡迎留言討論,也歡迎關注我~
我也會關注你的哦!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章