HashMap實現原理及源碼分析

HashMap是什麼
HashMap是Java經常使用的用來儲存鍵值對的數據結構,它是線程不安全的,能夠儲存null鍵值,這些你們常常用,也都知道,接下來根據源碼分析一下HashMap的實現。java

1、實現原理
HashMap的主幹是一個Entry數組。Entry是HashMap的基本組成單元,每個Entry包含一個key-value鍵值對。鍵值對的對象實現以下:數組

//HashMap的主幹數組,能夠看到就是一個Entry數組,初始值爲空數組{},主幹數組的長度必定是2的次冪,至於爲何這麼作,後面會有詳細分析。
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,避免重複計算

        /**
         * Creates new entry.
         */
        Entry(int h, K k, V v, Entry<K,V> n) {
            value = v;
            next = n;
            key = k;
            hash = h;
        }

簡單來講,HashMap由數組+鏈表組成的,數組是HashMap的主體,鏈表則是主要爲了解決哈希衝突而存在的,若是定位到的數組位置不含鏈表(當前entry的next指向null),那麼對於查找,添加等操做很快,僅需一次尋址便可;若是定位到的數組包含鏈表,對於添加操做,其時間複雜度爲O(n),首先遍歷鏈表,存在即覆蓋,不然新增;對於查找操做來說,仍需遍歷鏈表,而後經過key對象的equals方法逐一比對查找。因此,性能考慮,HashMap中的鏈表出現越少,性能纔會越好。數據結構

其餘幾個重要字段函數

/實際存儲的key-value鍵值對的個數
transient int size;
//閾值,當table == {}時,該值爲初始容量(初始容量默認爲16);當table被填充了,也就是爲table分配內存空間後,threshold通常爲 capacity*loadFactory。HashMap在進行擴容時須要參考threshold,後面會詳細談到
int threshold;
//負載因子,表明了table的填充度有多少,默認是0.75
final float loadFactor;
//用於快速失敗,因爲HashMap非線程安全,在對HashMap進行迭代時,若是期間其餘線程的參與致使HashMap的結構發生變化了(好比put,remove等操做),須要拋出異常ConcurrentModificationException
transient int modCount;

HashMap有4個構造器,其餘構造器若是用戶沒有傳入initialCapacity 和loadFactor這兩個參數,會使用默認值源碼分析

initialCapacity默認爲16,loadFactory默認爲0.75性能

咱們看下其中一個this

public HashMap(int initialCapacity, float loadFactor) {
     //此處對傳入的初始容量進行校驗,最大不能超過MAXIMUM_CAPACITY = 1<<30(230)
        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();//init方法在HashMap中沒有實際實現,不過在其子類如 linkedHashMap中就會有對應實現
    }

2、put方法,寫入鍵值對

public V put(K key, V value){

 //若是table數組爲空數組{},進行數組填充(爲table分配實際內存空間),入參爲threshold,此時threshold爲initialCapacity 默認是1<<4(24=16)
    if (table == EMPTY_TABLE) {
            inflateTable(threshold);
     }
 
 //若是 key 爲 null,調用 putForNullKey 方法寫入null鍵的值
 if (key == null){
    return putForNullKey(value);
}

//根據 key 的 keyCode 計算 Hash 值 
int hash = hash(key.hashCode());
//查找hash值在table中的索引
int i = indexFor(hash, table.length);
// 若是 i 索引處的 Entry 不爲 null,經過循環不斷遍歷鏈表查找是否在鏈表中有相同key的Entry
for (Entry<K,V> e = tablei; e != null; e = e.next) {
    Object k;
    //找到與插入的值的key和hash相同的Entry
    if (e.hash == hash && ((k = e.key) == key|| key.equals(k)){

        //key值相同時直接替換value值,跳出函數
        V oldValue = e.value;
        e.value = value;
    
       e.recordAccess(this);
 
       return oldValue;
     
   }
    
}
// 若是 i 索引處的 Entry 爲 null 或者key的hash值相同而key不一樣  ,則須要新增Entry
modCount++; 
// 將 key、value 添加到 i 索引處
addEntry(hash, key, value, i); 
return null; 
}

在put方法中解決hash碰撞的方式很清楚,即當兩個entry的hash值相同時,須要對key值是否相同進行判斷,只有key和hash都相同,才能進行修改,不然認爲不是同一個entry。spa

 先來看看inflateTable這個方法線程

private void inflateTable(int toSize) {
        int capacity = roundUpToPowerOf2(toSize);//capacity必定是2的次冪
        threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);//此處爲threshold賦值,取capacity*loadFactor和MAXIMUM_CAPACITY+1的最小值,capaticy必定不會超過MAXIMUM_CAPACITY,除非loadFactor大於1
        table = new Entry[capacity];
        initHashSeedAsNeeded(capacity);
    }

 inflateTable這個方法用於爲主幹數組table在內存中分配存儲空間,經過roundUpToPowerOf2(toSize)能夠確保capacity爲大於或等於toSize的最接近toSize的二次冪,好比toSize=13,則capacity=16;to_size=16,capacity=16;to_size=17,capacity=32.

private static int roundUpToPowerOf2(int number) {
        // assert number >= 0 : "number must be non-negative";
        return number >= MAXIMUM_CAPACITY
                ? MAXIMUM_CAPACITY
                : (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
    }

roundUpToPowerOf2中的這段處理使得數組長度必定爲2的次冪,Integer.highestOneBit是用來獲取最左邊的bit(其餘bit位爲0)所表明的數值.

hash函數

//這是一個神奇的函數,用了不少的異或,移位等運算,對key的hashcode進一步進行計算以及二進制位的調整等來保證最終獲取的存儲位置儘可能分佈均勻
final int hash(Object k) {
        int h = hashSeed;
        if (0 != h && k instanceof String) {
            return sun.misc.Hashing.stringHash32((String) k);
        }

        h ^= k.hashCode();

        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }

以上hash函數計算出的值,經過indexFor進一步處理來獲取實際的存儲位置

  /**
     * 返回數組下標
     */
    static int indexFor(int h, int length) {
        return h & (length-1);
    }

再來看看addEntry的實現:

void addEntry(int hash, K key, V value, int bucketIndex) {
        if ((size >= threshold) && (null != table[bucketIndex])) {
            resize(2 * table.length);//當size超過臨界閾值threshold,而且即將發生哈希衝突時進行擴容
            hash = (null != key) ? hash(key) : 0;
            bucketIndex = indexFor(hash, table.length);
        }

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

經過以上代碼可以得知,當發生哈希衝突而且size大於閾值的時候,須要進行數組擴容,擴容時,須要新建一個長度爲以前數組2倍的新的數組,而後將當前的Entry數組中的元素所有傳輸過去,擴容後的新數組長度爲以前的2倍,因此擴容相對來講是個耗資源的操做。

咱們來繼續看上面提到的resize方法

void resize(int newCapacity) {
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }

        Entry[] newTable = new Entry[newCapacity];
        transfer(newTable, initHashSeedAsNeeded(newCapacity));
        table = newTable;
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }

若是數組進行擴容,數組長度發生變化,而存儲位置 index = h&(length-1),index也可能會發生變化,須要從新計算index,咱們先來看看transfer這個方法

void transfer(Entry[] newTable, boolean rehash) {
        int newCapacity = newTable.length;
     //for循環中的代碼,逐個遍歷鏈表,從新計算索引位置,將老數組數據複製到新數組中去(數組不存儲實際數據,因此僅僅是拷貝引用而已)
        for (Entry<K,V> e : table) {
            while(null != e) {
                Entry<K,V> next = e.next;
                if (rehash) {
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
                int i = indexFor(e.hash, newCapacity);
          //將當前entry的next鏈指向新的索引位置,newTable[i]有可能爲空,有可能也是個entry鏈,若是是entry鏈,直接在鏈表頭部插入。
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            }
        }
    }

這個方法將老數組中的數據逐個鏈表地遍歷,扔到新的擴容後的數組中,咱們的數組索引位置的計算是經過 對key值的hashcode進行hash擾亂運算後,再經過和 length-1進行位運算獲得最終數組索引位置。

3、get方法

public V get(Object key) 
{ 
// 若是 key 是 null,調用 getForNullKey 取出null的 value 
if (key == null) 
      return getForNullKey(); 
// 根據該 key 的 hashCode 值計算它的 hash 碼 
int hash = hash(key.hashCode()); 
// 直接取出 table 數組中指定索引處的值, // 搜索該 Entry 鏈的下一個對象 
for (Entry<K,V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) 
{ 
    Object k; 
    // 若是該 Entry 的 key和hash 與被搜索 key 相同 
   if (e.hash == hash && ((k = e.key) == key || key.equals(k))) 
    return e.value; 
   } 
return null; 
}

能夠看出,get方法的實現相對簡單,key(hashcode)-->hash-->indexFor-->最終索引位置,找到對應位置table[i],再查看是否有鏈表,遍歷鏈表,經過key的equals方法比對查找對應的記錄。要注意的是,有人以爲上面在定位到數組位置以後而後遍歷鏈表的時候,e.hash == hash這個判斷不必,僅經過equals判斷就能夠。其實否則,試想一下,若是傳入的key對象重寫了equals方法卻沒有重寫hashCode,而恰巧此對象定位到這個數組位置,若是僅僅用equals判斷多是相等的,但其hashCode和當前對象不一致,這種狀況,根據Object的hashCode的約定,不能返回當前對象,而應該返回null。

4、總結

HashMap的工做原理

HashMap基於hashing原理,咱們經過put()和get()方法儲存和獲取對象。當咱們將鍵值對傳遞給put()方法時,它調用鍵對象的hashCode()方法來計算hashcode,讓後找到bucket位置來儲存值對象。當獲取對象時,經過鍵對象的equals()方法找到正確的鍵值對,而後返回值對象。HashMap使用鏈表來解決碰撞問題,當發生碰撞了,對象將會儲存在鏈表的下一個節點中。 HashMap在每一個鏈表節點中儲存鍵值對對象。

相關文章
相關標籤/搜索