java集合的底層原理(Map的底層原理 一)

 此文承接  java集合的底層原理(List的底層原理),具體可以此文的開頭講述,此處簡要概述的map的結構如下

Map 接口 鍵值對的集合 (雙列集合)
├———Hashtable 接口實現類, 同步, 線程安全
├———HashMap 接口實現類 ,沒有同步, 線程不安全-
│—————–├ LinkedHashMap 雙向鏈表和哈希表實現
│—————–└ WeakHashMap
├ ——–TreeMap 紅黑樹對所有的key進行排序
└———IdentifyHashMap
 

一、HashMap

1.1  概述  

       HashMap基於Map接口實現,元素以鍵值對的方式存儲,並且允許使用null 建和null值, 因爲key不允許重複,因此只能有一個鍵爲null,另外HashMap不能保證放入元素的順序,它是無序的,和放入的順序並不能相同。HashMap是線程不安全的

       在java編程語言中,最基本的結構就是兩種,一個是數組,另外一個是模擬指針(引用),所有的數據結構都可以用這兩個基本結構來構造的,HashMap也不例外。HashMap實際上是一個“鏈表散列”的數據結構,即數組和鏈表的結合體。


從上圖中可以看出,HashMap底層就是一個數組結構,數組中的每一項又是一個鏈表。當新建一個HashMap的時候,就會初始化一個數組。

 java源碼如下:

/**
 * The table, resized as necessary. Length MUST Always be a power of two.
 */
transient Entry[] table;

static class Entry<K,V> implements Map.Entry<K,V> {
    final K key;
    V value;
    Entry<K,V> next;
    final int hash;
    ……
}

1.2 HashMap實現存儲讀取元素

   先看存儲源碼:

public V put(K key, V value) {
    //調用putVal()方法完成
    return putVal(hash(key), key, value, false, true);
}
 
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    //判斷table是否初始化,否則初始化操作
    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);
                    //鏈表長度8,將鏈表轉化爲紅黑樹存儲
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                //key存在,直接覆蓋
                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;
}

根據hash值得到這個元素在數組中的位置(即下標),如果數組該位置上已經存放有其他元素了,那麼在這個位置上的元素將以鏈表的形式存放,新加入的放在鏈頭,最先加入的放在鏈尾。如果數組該位置上沒有元素,就直接將該元素放到此數組中的該位置上。

hash(int h)方法根據key的hashCode重新計算一次散列。此算法加入了高位計算,防止低位不變,高位變化時,造成的hash衝突。

我們可以看到在HashMap中要找到某個元素,需要根據key的hash值來求得對應數組中的位置。如何計算這個位置就是hash算法。前面說過HashMap的數據結構是數組和鏈表的結合,所以我們當然希望這個HashMap裏面的元素位置儘量的分佈均勻些,儘量使得每個位置上的元素數量只有一個,那麼當我們用hash算法求得這個位置的時候,馬上就可以知道對應位置的元素就是我們要的,而不用再去遍歷鏈表,這樣就大大優化了查詢的效率。

根據上面 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 鏈的頭部——具體說明繼續看 addEntry() 方法的說明。

讀取元素源碼 

public V get(Object key) {
    if (key == null)
        return getForNullKey();
    int hash = hash(key.hashCode());
    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.equals(k)))
            return e.value;
    }
    return null;
}

從HashMap中get元素時,首先計算key的hashCode,找到數組中對應位置的某一元素,然後通過key的equals方法在對應位置的鏈表中找到需要的元素。

總結起來就是:

       HashMap 在底層將 key-value 當成一個整體進行處理,這個整體就是一個 Entry 對象。HashMap 底層採用一個 Entry[] 數組來保存所有的 key-value 對,當需要存儲一個 Entry 對象時,會根據hash算法來決定其在數組中的存儲位置,在根據equals方法決定其在該數組位置上的鏈表中的存儲位置;當需要取出一個Entry時,也會根據hash算法找到其在數組中的存儲位置,再根據equals方法從該位置上的鏈表中取出該Entry。

1.3 HashMap的擴容

       當hashmap中的元素越來越多的時候,碰撞的機率也就越來越高(因爲數組的長度是固定的),所以爲了提高查詢的效率,就要對hashmap的數組進行擴容,數組擴容這個操作也會出現在ArrayList中,所以這是一個通用的操作,很多人對它的性能表示過懷疑,不過想想我們的“均攤”原理,就釋然了,而在hashmap數組擴容之後,最消耗性能的點就出現了:原數組中的數據必須重新計算其在新數組中的位置,並放進去,這就是resize。

所以擴容必須滿足兩條件

  • 存放新值的時候當前已有元素必須大於閾值;
  • 存放新值的時候當前存放數據發生hash碰撞(當前key計算的hash值計算出的數組索引位置已經存在值)

       那麼hashmap什麼時候進行擴容呢?當hashmap中的元素個數超過數組大小*loadFactor時,就會進行數組擴容,loadFactor的默認值爲0.75,也就是說,默認情況下,數組大小爲16,那麼當hashmap中元素個數超過16*0.75=12的時候,就把數組的大小擴展爲2*16=32,即擴大一倍,然後重新計算每個元素在數組中的位置,而這是一個非常消耗性能的操作,所以如果我們已經預知hashmap中元素的個數,那麼預設元素的個數能夠有效的提高hashmap的性能。比如說,我們有1000個元素new HashMap(1000), 但是理論上來講new HashMap(1024)更合適,不過上面annegu已經說過,即使是1000,hashmap也自動會將其設置爲1024。 但是new HashMap(1024)還不是更合適的,因爲0.75*1000 < 1000, 也就是說爲了讓0.75 * size > 1000, 我們必須這樣new HashMap(2048)才最合適,既考慮了&的問題,也避免了resize的問題。

總結:

1、HashMap是基於哈希表的Map接口的非同步實現,允許使用null值和null鍵,但不保證映射的順序。
2、底層使用數組實現,數組中每一項是個單向鏈表,即數組和鏈表的結合體;當鏈表長度大於一定閾值(8)時,鏈表轉換爲紅黑樹(在Jdk1.8的優化),這樣減少鏈表查詢時間。
3、HashMap在底層將key-value當成一個整體進行處理,這個整體就是一個Node對象。HashMap底層採用一個Node[]數組來保存所有的key-value對,當需要存儲一個Node對象時,會根據key的hash算法來決定其在數組中的存儲位置,在根據equals方法決定其在該數組位置上的鏈表中的存儲位置;當需要取出一個Node時,也會根據key的hash算法找到其在數組中的存儲位置,再根據equals方法從該位置上的鏈表中取出該Node。
4、HashMap進行數組擴容需要重新計算擴容後每個元素在數組中的位置,很耗性能
5、採用了Fail-Fast機制,通過一個modCount值記錄修改次數,對HashMap內容的修改都將增加這個值。迭代器初始化過程中會將這個值賦給迭代器的expectedModCount,在迭代過程中,判斷modCount跟expectedModCount是否相等,如果不相等就表示已經有其他線程修改了Map,馬上拋出異常
      

二、HashTable

2.1 概述

和HashMap一樣,HashTable也是一個散列表,它存儲的內容是鍵值對映射。HashTable繼承於Dictionary,實現了Map、Cloneable、java.io.Serializable接口。HashTable的函數都是同步的,這意味着它是線程安全的。它的Key、Value都不可以爲null。此外,HashTable中的映射不是有序的。

HashTable的實例有兩個參數影響其性能:初始容量和加載因子。容量是哈希表中桶的數量,初始容量就是哈希表創建時的容量。注意,哈希表的狀態爲open:在發生“哈希衝突”的情況下,單個桶會存儲多個條目,這些條目必須按順序搜索。加載因子是對哈希表在其容量自動增加之前可以達到多滿的一個尺度。初始容量和加載因子這兩個參數只是對該實現的提示。關於何時以及是否調用rehash方法的具體細節則依賴於該實現。通常,默認加載因子是0.75。
 

2.2  數據結構

HashTable與Map關係如下

 

public class Hashtable<K,V>  
    extends Dictionary<K,V>  
    implements Map<K,V>, Cloneable, java.io.Serializable 

HashTable並沒有去繼承AbstractMap,而是選擇繼承了Dictionary類,Dictionary是個被廢棄的抽象類

 

2.3  實現原理

成員變量跟HashMap基本類似,但是HashMap更加規範,HashMap內部還定義了一些常量,比如默認的負載因子,默認的容量,最大容量等。

public Hashtable(int initialCapacity, float loadFactor) {//可指定初始容量和加載因子  
        if (initialCapacity < 0)  
            throw new IllegalArgumentException("Illegal Capacity: "+  
                                               initialCapacity);  
        if (loadFactor <= 0 || Float.isNaN(loadFactor))  
            throw new IllegalArgumentException("Illegal Load: "+loadFactor);  
        if (initialCapacity==0)  
            initialCapacity = 1;//初始容量最小值爲1  
        this.loadFactor = loadFactor;  
        table = new Entry[initialCapacity];//創建桶數組  
        threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);//初始化容量閾值  
        useAltHashing = sun.misc.VM.isBooted() &&  
                (initialCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);  
    }  
    /** 
     * Constructs a new, empty hashtable with the specified initial capacity 
     * and default load factor (0.75). 
     */  
    public Hashtable(int initialCapacity) {  
        this(initialCapacity, 0.75f);//默認負載因子爲0.75  
    }  
    public Hashtable() {  
        this(11, 0.75f);//默認容量爲11,負載因子爲0.75  
    }  
    /** 
     * Constructs a new hashtable with the same mappings as the given 
     * Map.  The hashtable is created with an initial capacity sufficient to 
     * hold the mappings in the given Map and a default load factor (0.75). 
     */  
    public Hashtable(Map<? extends K, ? extends V> t) {  
        this(Math.max(2*t.size(), 11), 0.75f);  
        putAll(t);  
    }  

爲避免擴容帶來的性能問題,建議指定合理容量。跟HashMap一樣,HashTable內部也有一個靜態類叫Entry,其實是個鍵值對,保存了鍵和值的引用。也可以理解爲一個單鏈表的節點,因爲其持有下一個Entry對象的引用

 

2.3 存取實現

  存數據

public synchronized V put(K key, V value) {//向哈希表中添加鍵值對  
        // Make sure the value is not null  
        if (value == null) {//確保值不能爲空  
            throw new NullPointerException();  
        }  
        // Makes sure the key is not already in the hashtable.  
        Entry tab[] = table;  
        int hash = hash(key);//根據鍵生成hash值---->若key爲null,此方法會拋異常  
        int index = (hash & 0x7FFFFFFF) % tab.length;//通過hash值找到其存儲位置  
        for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) {/遍歷鏈表  
            if ((e.hash == hash) && e.key.equals(key)) {//若鍵相同,則新值覆蓋舊值  
                V old = e.value;  
                e.value = value;  
                return old;  
            }  
        }  
        modCount++;  
        if (count >= threshold) {//當前容量超過閾值。需要擴容  
            // Rehash the table if the threshold is exceeded  
            rehash();//重新構建桶數組,並對數組中所有鍵值對重哈希,耗時!  
            tab = table;  
            hash = hash(key);  
            index = (hash & 0x7FFFFFFF) % tab.length;//這裏是取摸運算  
        }  
        // Creates the new entry.  
        Entry<K,V> e = tab[index];  
        //將新結點插到鏈表首部  
        tab[index] = new Entry<>(hash, key, value, e);//生成一個新結點  
        count++;  
        return null;  
    }  

取數據

public synchronized V get(Object key) {//根據鍵取出對應索引  
      Entry tab[] = table;  
      int hash = hash(key);//先根據key計算hash值  
      int index = (hash & 0x7FFFFFFF) % tab.length;//再根據hash值找到索引  
      for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) {//遍歷entry鏈  
          if ((e.hash == hash) && e.key.equals(key)) {//若找到該鍵  
              return e.value;//返回對應的值  
          }  
      }  
      return null;//否則返回null  
  }  

 

總結:

1、Hashtable是基於哈希表的Map接口的同步實現,不允許使用null值和null鍵底層使用數組實現,數組中每一項是個單鏈表,即數組和鏈表的結合體
2、Hashtable在底層將key-value當成一個整體進行處理,這個整體就是一個Entry對象。Hashtable底層採用一個Entry[]數組來保存所有的key-value對,當需要存儲一個Entry對象時,會根據key的hash算法來決定其在數組中的存儲位置,在根據equals方法決定其在該數組位置上的鏈表中的存儲位置;當需要取出一個Entry時,也會根據key的hash算法找到其在數組中的存儲位置,再根據equals方法從該位置上的鏈表中取出該Entry。
3、HashTable中若干方法都添加了synchronized關鍵字,也就意味着這個HashTable是個線程安全的類,這是它與HashMap最大的不同點

4、HashTable每次擴容都是舊容量的2倍加2,而HashMap爲舊容量的2倍。

   

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