HashMap實現原理及源碼分析

1、HashMap概述

  HashMap基於哈希表的 Map 接口的實現。此實現提供全部可選的映射操做,並容許使用 null 值和 null 鍵。(除了不一樣步和容許使用 null 以外,HashMap 類與 Hashtable 大體相同。)此類不保證映射的順序,特別是它不保證該順序恆久不變。
  值得注意的是HashMap不是線程安全的,若是想要線程安全的HashMap,能夠經過Collections類的靜態方法synchronizedMap得到線程安全的HashMap。java

 

 Map map = Collections.synchronizedMap(new HashMap());

 

2、HashMap的數據結構

  HashMap的底層主要是基於數組和鏈表來實現的,它之因此有至關快的查詢速度主要是由於它是經過計算散列碼來決定存儲的位置。HashMap中主要是經過key的hashCode來計算hash值的,只要hashCode相同,計算出來的hash值就同樣。若是存儲的對象對多了,就有可能不一樣的對象所算出來的hash值是相同的,這就出現了所謂的hash衝突。學過數據結構的同窗都知道,解決hash衝突的方法有不少,HashMap底層是經過鏈表來解決hash衝突的。數組

  圖中,0~15部分即表明哈希表,也稱爲哈希數組,數組的每一個元素都是一個單鏈表的頭節點,鏈表是用來解決衝突的,若是不一樣的key映射到了數組的同一位置處,就將其放入單鏈表中。安全

 

  從上圖咱們能夠發現哈希表是由數組+鏈表組成的,一個長度爲16的數組中,每一個元素存儲的是一個鏈表的頭結點Bucket桶。那麼這些元素是按照什麼樣的規則存儲到數組中呢。通常狀況是經過hash(key)%len得到,也就是元素的key的哈希值對數組長度取模獲得。好比上述哈希表中,12%16=12,28%16=12,108%16=12,140%16=12。因此十二、2八、108以及140都存儲在數組下標爲12的位置。
 
  HashMap其實也是一個線性的數組實現的,因此能夠理解爲其存儲數據的容器就是一個線性數組。這可能讓咱們很不解,一個線性的數組怎麼實現按鍵值對來存取數據呢?這裏HashMap有作一些處理。

 

  首先HashMap裏面實現一個靜態內部類Entry,其重要的屬性有 key , value, next,從屬性key,value咱們就能很明顯的看出來Entry就是HashMap鍵值對實現的一個基礎bean,咱們上面說到HashMap的基礎就是一個線性數組,這個數組就是Entry[],Map裏面的內容都保存在Entry[]裏面。

 咱們看看HashMap中Entry類的代碼:數據結構

    /** Entry是單向鏈表。    
     * 它是 「HashMap鏈式存儲法」對應的鏈表。    
     *它實現了Map.Entry 接口,即實現getKey(), getValue(), setValue(V value), equals(Object o), hashCode()這些函數  
    **/  
    static class Entry<K,V> implements Map.Entry<K,V> {    
        final K key;    
        V value;    
        // 指向下一個節點    
        Entry<K,V> next;    
        final int hash;    
   
        // 構造函數。    
        // 輸入參數包括"哈希值(h)", "鍵(k)", "值(v)", "下一節點(n)"    
        Entry(int h, K k, V v, Entry<K,V> n) {    
            value = v;    
            next = n;    
            key = k;    
            hash = h;    
        }    
   
        public final K getKey() {    
            return key;    
        }    
   
        public final V getValue() {    
            return value;    
        }    
   
        public final V setValue(V newValue) {    
            V oldValue = value;    
            value = newValue;    
            return oldValue;    
        }    
   
        // 判斷兩個Entry是否相等    
        // 若兩個Entry的「key」和「value」都相等,則返回true。    
        // 不然,返回false    
        public final boolean equals(Object o) {    
            if (!(o instanceof Map.Entry))    
                return false;    
            Map.Entry e = (Map.Entry)o;    
            Object k1 = getKey();    
            Object k2 = e.getKey();    
            if (k1 == k2 || (k1 != null && k1.equals(k2))) {    
                Object v1 = getValue();    
                Object v2 = e.getValue();    
                if (v1 == v2 || (v1 != null && v1.equals(v2)))    
                    return true;    
            }    
            return false;    
        }    
   
        // 實現hashCode()    
        public final int hashCode() {    
            return (key==null   ? 0 : key.hashCode()) ^ (value==null ? 0 : value.hashCode());    
        }    
   
        public final String toString() {    
            return getKey() + "=" + getValue();    
        }    
   
        // 當向HashMap中添加元素時,繪調用recordAccess()。    
        // 這裏不作任何處理    
        void recordAccess(HashMap<K,V> m) {    
        }    
   
        // 當從HashMap中刪除元素時,繪調用recordRemoval()。    
        // 這裏不作任何處理    
        void recordRemoval(HashMap<K,V> m) {    
        }    
    }

 

HashMap其實就是一個Entry數組,Entry對象中包含了鍵和值,其中next也是一個Entry對象,它就是用來處理hash衝突的,造成一個鏈表。app

 

3、HashMap源碼分析
關鍵屬性函數

 transient Entry[] table;//存儲元素的實體數組
  
 transient int size;//存放元素的個數
  
 int threshold; //臨界值   當實際大小超過臨界值時,會進行擴容threshold = 加載因子*容量
 
 final float loadFactor; //加載因子
  
 transient int modCount;//被修改的次數

 

  其中loadFactor加載因子是表示Hsah表中元素的填滿的程度.
  若:加載因子越大,填滿的元素越多,好處是,空間利用率高了,但:衝突的機會加大了.鏈表長度會愈來愈長,查找效率下降。
  反之,加載因子越小,填滿的元素越少,好處是:衝突的機會減少了,但:空間浪費多了.表中的數據將過於稀疏(不少空間還沒用,就開始擴容了)
  衝突的機會越大,則查找的成本越高.
  所以,必須在 "衝突的機會"與"空間利用率"之間尋找一種平衡與折衷. 這種平衡與折衷本質上是數據結構中有名的"時-空"矛盾的平衡與折衷.
  若是機器內存足夠,而且想要提升查詢速度的話能夠將加載因子設置小一點;相反若是機器內存緊張,而且對查詢速度沒有什麼要求的話能夠將加載因子設置大一點。不過通常咱們都不用去設置它,讓它取默認值0.75就行了。源碼分析

構造方法:this

    /**
     * Constructs an empty <tt>HashMap</tt> with the specified initial
     * capacity and load factor.
     *
     * @param  initialCapacity the initial capacity
     * @param  loadFactor      the load factor
     * @throws IllegalArgumentException if the initial capacity is negative
     *         or the load factor is nonpositive
     */
    public HashMap(int initialCapacity, float loadFactor) {
        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();
    }

 

    /**
     * Constructs an empty <tt>HashMap</tt> with the specified initial
     * capacity and the default load factor (0.75).
     *
     * @param  initialCapacity the initial capacity.
     * @throws IllegalArgumentException if the initial capacity is negative.
     */
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

 

    /**
     * Constructs an empty <tt>HashMap</tt> with the default initial capacity
     * (16) and the default load factor (0.75).
     */
    public HashMap() {
        this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
    }

  咱們能夠看到在構造HashMap的時候若是咱們指定了加載因子和初始容量的話就調用第一個構造方法,不然的話就是用默認的。默認初始容量爲16,默認加載因子爲0.75。咱們能夠看到上面代碼中13-15行,這段代碼的做用是確保容量爲2的n次冪,使capacity爲大於initialCapacity的最小的2的n次冪,至於爲何要把容量設置爲2的n次冪,咱們等下再看。spa

重點分析下HashMap中用的最多的兩個方法put和get。線程

主幹方法:

put(K key, V value)方法,向HashMap中塞值。
public V put(K key, V value) {
// 若「key爲null」,則將該鍵值對添加到table[0]中。 if (key == null) return putForNullKey(value); // 若「key不爲null」,則計算該key的哈希值,而後將其添加到該哈希值對應的鏈表中。 int hash = hash(key.hashCode()); //搜索指定hash值在對應table中的索引 int i = indexFor(hash, table.length); // 循環遍歷Entry數組,若「該key」對應的鍵值對已經存在,則用新的value取代舊的value。而後退出! 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))) { //若是key相同則覆蓋並返回舊值 V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } //修改次數+1 modCount++; //將key-value添加到table[i]處 addEntry(hash, key, value, i); return null; }

 

  上面程序中用到了一個重要的內部接口:Map.Entry,每一個 Map.Entry 其實就是一個 key-value 對。從上面程序中能夠看出:當系統決定存儲 HashMap 中的 key-value 對時,徹底沒有考慮 Entry 中的 value,僅僅只是根據 key 來計算並決定每一個 Entry 的存儲位置。這也說明了前面的結論:咱們徹底能夠把 Map 集合中的 value 當成 key 的附屬,當系統決定了 key 的存儲位置以後,value 隨之保存在那裏便可。

private V putForNullKey(V value) {
          for (Entry<K,V> e = table[0]; e != null; e = e.next) {
              if (e.key == null) {   //若是有key爲null的對象存在,則覆蓋掉
                  V oldValue = e.value;
                  e.value = value;
                  e.recordAccess(this);
                  return oldValue;
             }
         }
         modCount++;
         addEntry(0, null, value, 0); //若是鍵爲null的話,則hash值爲0
         return null;
 }

 

注意:若是key爲null的話,hash值爲0,對象存儲在數組中索引爲0的位置。即table[0]

咱們再回去看看put方法中第4行,它是經過key的hashCode值計算hash碼,下面是計算hash碼的函數:

 

//計算hash值的方法 經過鍵的hashCode來計算
     static int hash(int h) {
         // This function ensures that hashCodes that differ only by
         // constant multiples at each bit position have a bounded
         // number of collisions (approximately 8 at default load factor).
         h ^= (h >>> 20) ^ (h >>> 12);
         return h ^ (h >>> 7) ^ (h >>> 4);
     }

 

獲得hash碼以後就會經過hash碼去計算出應該存儲在數組中的索引,計算索引的函數以下:

     static int indexFor(int h, int length) { //根據hash值和數組長度算出索引值
         return h & (length-1);  //這裏不能隨便算取,用hash&(length-1)是有緣由的,這樣能夠確保算出來的索引是在數組大小範圍內,不會超出
     }

 

 

 

 

 

 

// 若「key爲null」,則將該鍵值對添加到table[0]中。
相關文章
相關標籤/搜索