HashMap的底層實現原理

HsahMap的實現原理

簡要概括

  • HashMap 基於 Hash 算法實現的,底層是由數組+鏈表/紅黑樹構成的,我們通過 put(key,value)存儲,get(key)來獲取。當傳入 key 時,HashMap 會根據 key. hashCode() 計算出 hash 值,根據 hash 值將 value 保存在 bucket 裏。當計算出的 hash 值相同時,我們稱之爲 hash 衝突,HashMap 的做法是用鏈表和紅黑樹存儲相同 hash 值的 value。當 hash 衝突的個數比較少時,使用鏈表,否則使用紅黑樹。

HashMap的存取實現

  • HashMap通過鍵值對實現存取。
  • put()方法:對key做null檢查。如果key是null,會被存儲到table[0],因爲null的hash值總是0。 key的hashcode()方法會被調用,然後計算hash值。hash值用來找到存儲Entry對象的數組的索引。有時候hash函數可能寫的很不好,所以JDK的設計者添加了另一個叫做hash()的方法,它接收剛纔計算的hash值作爲參數。
 public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
    /**
     * Implements Map.put and related methods.
     *
     * @param hash hash for key
     * @param key the key
     * @param value the value to put
     * @param onlyIfAbsent if true, don't change existing value
     * @param evict if false, the table is in creation mode.
     * @return previous value, or null if none
     */
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
  		 Node<K,V>[] tab; 
  		 Node<K,V> p; 
  		 int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }
  • Get():對key進行null檢查。如果key是null,table[0]這個位置的元素將被返回。
    key的hashcode()方法被調用,然後計算hash值。indexFor(hash,table.length)用來計算要獲取的Entry對象在table數組中的精確的位置,使用剛纔計算的hash值。在獲取了table數組的索引之後,會迭代鏈表,調用equals()方法檢查key的相等性,如果equals()方法返回true,get方法返回Entry對象的value,否則,返回null。
 public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }

    /**
     * Implements Map.get and related methods.
     *
     * @param hash hash for key
     * @param key the key
     * @return the node, or null if none
     */
    final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            if ((e = first.next) != null) {
                if (first instanceof TreeNode)
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }

補充:

  1. HashMap有一個叫做Entry的內部類,它用來存儲key-value對。
  2. 上面的Entry對象是存儲在一個叫做table的Entry數組中。
  3. table的索引在邏輯上叫做“桶”(bucket),它存儲了鏈表的第一個元素。
  4. key的hashcode()方法用來找到Entry對象所在的桶。
  5. 如果兩個key有相同的hash值,他們會被放在table數組的同一個桶裏面。
  6. key的equals()方法用來確保key的唯一性。

有關知識的具體解析

一、Map的幾種類型

在這裏插入圖片描述

  • Map就是一個值key對應一個value。
  • Hashtable(線程安全)和HashMap(非線程安全)在代碼實現上,基本上是一樣的。現在Hashtable已經過時了(小寫的t,因爲sun當時的一個失誤,因爲是JDK1.0的產物,所以不方便改)。
  • ConcurrentHashMap也是線程安全的,但性能比HashTable好很多,Hashtable是鎖整個Map對象,而ConcurrentHashMap是鎖Map的部分結構。

二、什麼是哈希表?

  • 利用數組尋址容易,但插入和刪除困難。而鏈表是:尋址困難,插入和刪除容易。而哈希表便綜合兩者的特性,是一種尋址容易,插入刪除也容易的數據結構。
  • 哈希表有多種不同的實現方法,最常用的方法—— 拉鍊法,可以理解爲“鏈表的數組”
    在這裏插入圖片描述
  • 一個長度爲16的數組中,每個元素存儲的是一個鏈表的頭結點。這些元素是按照什麼樣的規則存儲到數組中呢?一般情況是通過hash(key)%len獲得,也就是元素的key的哈希值對數組長度取模得到。
  • 比如上述哈希表中12%16=12 , 28%16=12 , 108%16=12 , 140%16=12。所以12、28、108,140都存儲在數組下標爲12的位置。
  • HashMap其實也是一個線性數組(Entry[])實現的,所以可以理解爲其存儲數據的容器就是一個線性數組。但是一個線性的數組怎麼實現按鍵值對來存取數據呢?這裏HashMap是做了一些處理的。

三、什麼是哈希算法?

  • Hash算法雖然被稱爲算法,但實際上它更像是一種思想。Hash算法沒有一個固定的公式,只要符合散列思想的算法都可以被稱爲是Hash算法。
  • 哈希(hash)算法又稱爲散列算法,通過hash算法,可以將任意長度的信息轉換成一個固定長度的二進制數據,我們經常會使用十六進制值來表示轉換後的信息。
  • 比如,數字123,使用md5的hash算法後,得到十六進制的值:202cb962ac59075b964b07152d234b70
  • 哈希算法的特點:
    (1)不同的信息,理論上得到的hash值不同,我們稱之爲“無碰撞”,或者發生“碰撞”的概率非常小。
    (2)不可逆,hash算法是單向的,從hash值反向推導出原始信息是很困難的。所以,有些系統中,我們可以使用hash算法對密碼進行處理後保存。
  • 哈希算法的應用

四、什麼是紅黑樹?

  • 二叉樹(BST)
    ①左子樹結點的值小於等於根節點的值。
    ②右子樹結點的值大於等於根節點的值。
    ③左右子樹分開來也是單獨的二叉樹。
    在這裏插入圖片描述
  • 紅黑樹(RBT):紅黑樹是一種自平衡的二叉樹,除了符合二叉樹的基本特徵之外還引入了一些附加的條件。
    ①節點是紅色或黑色。
    ②根節點是黑色。
    ③每個葉子節點都是黑色的空節點(NIL節點)。
    ④每個紅色節點的兩個子節點都是黑色。(從每個葉子到根的所有路徑上不能有兩個連續的紅色節點)。
    ⑥從任一節點到其每個葉子的所有路徑都包含相同數目的黑色節點。
    在這裏插入圖片描述

五、HashMap 和 Hashtable 有什麼區別?

  • HashMap是非線程安全的,HashMap是Map的一個實現類,是將鍵映射到值的對象,不允許鍵值重複。允許空鍵和空值;由於非線程安全,HashMap的效率要較 Hashtable 的效率高一些。
  • Hashtable 是線程安全的一個集合,不允許 null 值作爲一個 key 值或者value 值。
  • Hashtable是sychronized,多個線程訪問時不需要自己爲它的方法實現同步,而HashMap 在被多個線程訪問的時候需要自己爲它的方法實現同步。
  • 一般現在不建議用Hashtable:
    ①注意是小寫的t,這是sun公司的一個失誤,但是由於是JDK1.0的產物,所以沒有改
    ②是Hashtable是遺留類,內部實現很多沒優化和冗餘。
    ③即使在多線程環境下,現在也有同步的ConcurrentHashMap替代,沒有必要因爲是多線程而用HashTable。

如何解決hash衝突

產生hash衝突的原因

  • 當我們通過put(key, value)向hashmap中添加元素時,需要通過hash函數確定元素究竟應該放置在數組中的哪個位置,因爲不同的元素可能通過hashcode()計算得到的哈希值相同,那麼不同的元素被放置在了數據的同一個位置時,後放入的元素會以鏈表的形式,插在前一個元素的尾部,這個時候我們稱發生了hash衝突。

解決方法

  • 事實上,想讓hash衝突完全不發生,是不太可能的,我們能做的只是儘可能的降低hash衝突發生的概率。
    ①開放定址法
    ②鏈地址法(拉鍊法)
    Java 中 HashMap 解決 Hash 衝突就是利用了這個方法,具體實現這裏暫時不做詳解,可以參考 Jdk HashMap 源碼進行理解。
    ③再哈希法
    ④建立公共溢出區
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章