HashMap原理及源碼分析

注:本文根據網絡和部分書籍整理基於JDK1.7書寫與1.8版本對比介紹HashMap基本原理,文中源碼爲JDK 1.7
       本文內容如有雷同敬請諒解,歡迎指正文中的錯誤之處。

哈希表

       哈希就是把任意長度的輸入,通過散列算法,變成固定長度的輸出,該輸出就是散列值。哈希法又稱散列法、雜湊法以及關鍵字地址計算法等,相應的表成爲哈希表。哈希表是一種根據關鍵碼去尋找值的數據映射結構。該結構通過關鍵碼映射的位置去尋找存放值,提供快速的插入操作和查找操作。哈希表基於數組的,創建後難於擴展,某些哈希表被基本填滿時,性能下降嚴重而且沒有一種簡便的方法可以以任何一種順序(例如從小到大)遍歷表中數據項。

      哈希表的特點:關鍵字在表中位置和它之間存在一種確定的關係。

      Hash主要有兩種應用:加密和壓縮。
      加密方面:Hash哈希是把一些不同長度的信息轉化成雜亂的128位的編碼,這些編碼值叫做HASH值,最廣泛應用的Hash算法有MD4、MD5、SHA-1 和 SHA-2。
      壓縮方面:Hash哈希是指把一個大範圍映射到一個小範圍,往往是爲了節省空間,使得數據容易保存。

哈希函數

      哈希函數:在元素的關鍵字K和元素的位置P之間建立一個對應關係f,使得P=f(K),其中稱這個函數f(key)爲哈希函數。

      哈希函數的目的是得到關鍵字值的範圍,用一種方式轉化爲數組的下標值,儘可能地保證計算簡單和散列地址分佈均勻。哈希函數的構造原則是:函數本身便於計算、計算出來的地址分佈均勻。主要優點是:速度,對抗碰撞不太看中,只要保證hash均勻分佈就可以(比如hashmap,hash值(key)存在的目的是加速鍵值對的查,key的作用是爲了將元素適當放在各個桶裏,對於抗碰撞的要求沒有那麼高。hash出來的key,只要保證value均勻的放在不同的桶裏就可以了。整個算法的set性能,直接與hash值產生的速度有關

      Hash函數逼近單向函數,所以可以用來對數據進行加密。(單項函數:如果某個函數在給定輸入的時候,很容易計算出其結果來;而當給定結果的時候,很難計算出輸入來)。不同的應用對Hash函數有着不同的要求:用於加密的Hash函數主要考慮它和單項函數的差距,而用於查找的Hash函數主要考慮它映射到小範圍的衝突率

      Hash的產生方式大體可以分爲三種基本方法:加法、乘法和移位。哈希函數中有許多乘法和除法是不可取的,求模算法作爲一種不可逆的計算方法,已經成爲了整個現代密碼學的根基

      Java中幾個常用的哈希碼(hashCode)的算法:

      Object類的hashCode. 返回對象的經過處理後的內存地址,由於每個對象的內存地址都不一樣,所以哈希碼也不一樣。這個是native方法,取決於JVM的內部設計,一般是某種C地址的偏移。

      String類的hashCode. 根據String類包含的字符串的內容,根據一種特殊算法返回哈希碼,只要字符串的內容相同,返回的哈希碼也相同。

      Integer等包裝類,返回的哈希碼就是Integer對象裏所包含的那個整數的數值,例如Integer i1=new Integer(100), i1.hashCode的值就是100 。由此可見,2個一樣大小的Integer對象,返回的哈希碼也一樣。

      int,char這樣的基礎類,它們不需要hashCode,如果需要存儲時,將進行自動裝箱操作,計算方法同上。

哈希衝突

      當關鍵字集合很大時,關鍵字值不同的元素可能會映像到哈希表的同一地址上,即K1!=K2,但f(K1)=f(K2),這種現象稱爲哈希衝突或哈希碰撞,實際中衝突是不可避免的,只能通過改進哈希函數的性能來減少衝突。

      處理衝突的方法:
      1、開放地址法(發生衝突,繼續尋找下一塊未被佔用的存儲地址)三種開放地址法:線性探測、二次探測、在哈希法
      2、鏈地址法,而HashMap即是採用了鏈地址法

HashMap原理

      HashMap 是一個散列表,它存儲的內容是鍵值對(key-value)映射,採用鏈地址法,也就是數組+鏈表的方式解決哈希衝突的

      HashMap 繼承於AbstractMap,實現了Map、Cloneable、java.io.Serializable接口。基於哈希表的 Map 接口的實現由數組+鏈表組成的,HashMap 的主體是一個Entry數組以 Key-Value 的形式存在,存儲的對象是 Entry (包含key,value,hash 和 next 四個屬性) ,Entry 來代表每個 HashMap 中的數據節點,鏈表則是主要爲了解決哈希衝突而存在的。在HashMap中,根據hash算法來計算key-value的存儲位置並進行快速存取。

      HashMap不是線程安全的它的key、value都可以爲null,其映射不是有序的。

      Entry對象唯一表示一個鍵值對,有四個屬性:

            int hash;  鍵對象的hash值

            final K key;  鍵對象

            V value; 值對象

            Entry<K,V> next; 指向鏈表中下一個Entry對象,可爲null,表示當前Entry對象在鏈表尾部

      注:1、Java 容器實際上包含的是引用變量,而這些引用變量指向了我們要實際保存的 Java 對象。

      2、Java8 中由 數組+鏈表+紅黑樹 組成。使用 Node,都是 key,value,hash 和 next 這四個屬性,不過,Node 只能用於鏈表的情況,當鏈表中的元素超過了 當鏈表長度超過TREEIFY_THRESHOLD 8 個以後 會將鏈表轉換爲紅黑樹使用 TreeNode。

重要屬性

      table : 一個Entry[] 數組類型,Entry實際上就是一個單向鏈表。哈希表的key-value鍵值對都是存儲在Entry數組中的。 
      size  :HashMap的大小,HashMap保存的鍵值對的數量。
      loadFactor :加載因子默認0.75
      capacity : 當前數組容量,始終保持 2^n,可以擴容,擴容後數組大小爲當前的 2 倍
      threshold :閾值,用於判斷是否需要調整HashMap的容量。threshold=容量*加載因子,當HashMap中存儲數據的數量達到threshold時,就需要rehash擴容重構

      注:初始容量 和 負載因子,這兩個參數是影響HashMap性能的重要參數初始容量是哈希表在創建時桶的數量,HashMap的底層數組長度是2的次冪(默認16);加載因子是哈希表在其容量自動增加之前可以達到多滿的一種尺度(默認 0.75)

put操作

     1、在第一個元素插入 HashMap 的時候做一次數組的初始化,就是先確定初始的數組大小,並計算數組擴容的閾值。
     2、當key爲null時,都放到table[0]

      注:HashMap最多隻允許一條Entry的鍵爲Null(多條會覆蓋),但允許多條Entry的值爲Null

     3、根據 key的 hashCode 值計算出一個位置,該位置就是此對象準備往數組中存放的位置。
      如果該位置沒有對象存在,就將此對象直接放進數組當中;如果該位置已經有對象存在了,則順着此存在的對象的鏈開始尋找(爲了判斷是否是否值相同,map不允許<key,value>鍵值對重複), 如果此鏈上有對象的話,再去使用 equals方法進行比較,如果對此鏈上的每個對象的 equals 方法比較都爲 false,則將該對象放到數組當中。

      注:1、Java7 是插入到鏈表的最前面,Java8 插入到鏈表的最後面。
      2、Java8中對鏈表長度增加了一個閾值TREEIFY_THRESHOLD,超過閾值8 個以後 鏈表將轉化爲紅黑樹使用 TreeNode,查詢時間複雜度降爲O(logn),提高了鏈表過長時的性能。

     4、當 size>=threshold(加載因子*當前容量時,會發生擴容resize操作一般情況下,容量將擴大至原來的兩倍。

     注:只有當 size>=threshold並且 table中的那個槽中已經有Entry時纔會發生resize。 Java7 是先擴容後插入新值的,Java8 先插值再擴容

    /**
     * Associates the specified value with the specified key in this map.
     * If the map previously contained a mapping for the key, the old
     * value is replaced.
     *
     * @param key key with which the specified value is to be associated
     * @param value value to be associated with the specified key
     * @return the previous value associated with <tt>key</tt>, or
     *         <tt>null</tt> if there was no mapping for <tt>key</tt>.
     *         (A <tt>null</tt> return can also indicate that the map
     *         previously associated <tt>null</tt> with <tt>key</tt>.)
     */
    public V put(K key, V value) {
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key);
        int i = indexFor(hash, table.length);
        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;
    }

get操作

     1、根據key計hash值找到相應的數組下標:hash & (length – 1)
         
 key(hashcode)-->hash-->indexFor-->最終索引位置,找到對應位置table[i]

     2、遍歷該數組位置處的鏈表,直到找到相等(==或equals)的 key。
           e.hash == hash很有必要,如果傳入的key對象重寫了equals方法卻沒有重寫hashCode

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

        return null == entry ? null : entry.getValue();
    }

    /**
     * Offloaded version of get() to look up null keys.  Null keys map
     * to index 0.  This null case is split out into separate methods
     * for the sake of performance in the two most commonly used
     * operations (get and put), but incorporated with conditionals in
     * others.
     */
    private V getForNullKey() {
        if (size == 0) {
            return null;
        }
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {
            if (e.key == null)
                return e.value;
        }
        return null;
    }
    
    /**
     * Returns the entry associated with the specified key in the
     * HashMap.  Returns null if the HashMap contains no mapping
     * for the key.
     */
    final Entry<K,V> getEntry(Object key) {
        if (size == 0) {
            return null;
        }

        int hash = (key == null) ? 0 : hash(key);
        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;
    }

擴展

     LinkedHashMap HashMap 的直接子類繼承了HashMap的所用特性(LinkedHashMap = HashMap + 雙向鏈表)。主要不同之處是:LinkedHashMap維護一個雙向鏈表保持了有序性重新定義了Entry,在Entry增加了兩個指針 before 和 after,用於維護Entry插入的先後順序

     HashSet 實現的是Set接口,所以不允許有重複的值。基於HashMap實現,底層使用HashMap來保存所有元素。

     HashTable 產生於JDK 1.1,與HashMap一樣都是基於哈希表來實現鍵值映射的工具類,實現了Map、Cloneable、Serializable三個接口,但是HashTable繼承自抽象類Dictionary。 HashMap和HashTable都使用哈希表來存儲鍵值對。在數據結構上是基本相同的。

     HashTable和HashMap在計算hash時都用到了一個叫hashSeed的變量。這是因爲映射到同一個hash桶內的Entry對象,是以鏈表的形式存在的,而鏈表的查詢效率比較低,所以HashMap/HashTable的效率對哈希衝突非常敏感,所以可以額外開啓一個可選hash(hashSeed),從而減少哈希衝突。這個優化在JDK 1.8中已經去掉了,因爲JDK 1.8中,映射到同一個哈希桶(數組位置)的Entry對象,使用了紅黑樹來存儲,從而大大加速了其查找效率。

     HashTable在遇到null時,會拋出NullPointerException異常;

     HashTable默認的初始大小爲11,之後每次擴充爲原來的2n+1,而HashMap默認的初始化大小爲16,之後每次擴充爲原來的2倍;

     HashTable是同步的容器,使用synchronized來保證線程安全;

    注:如果你不需要線程安全,那麼使用HashMap,如果需要線程安全,那麼使用ConcurrentHashMap。HashTable已經被淘汰了,不要在新的代碼中再使用它。

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