從原始碼的角度來談一談HashMap的內部實現原理

晨雨細曲發表於2018-08-19

HashMap可以說是我們一個熟悉又陌生的Java中常用的儲存資料的API。說他熟悉,是因為我們經常使用他,而說他陌生是因為我們大部分時間是隻知道他的使用,而並不知道他內部的原理,但是在面試考察的時候又最喜歡去問這個原理。今天,我就來從原始碼的角度,談談對HashMap的理解。

HashMap概述

hashMap的底層其實是基於一個陣列來進行資料的儲存和取出。他繼承於Map這個介面來實現,通過put和get方法來運算元據的存和取。具體對於hashMap的使用,這裡不在具體舉例說明,使用起來並不困難。不過在談到HashMap的內部原理之前,我們需要了解一下幾個名稱的意思。

1.initialCapacity。 這個翻譯為初始化容量。為hashMap的儲存的初始化空間的大小,我們可以通過構造方法來指定其大小,也可以不指定採用 預設大小16。這裡需要說明一下,一般來說,容器的大小為2的冪次方。至於為什麼會是2的冪次方,具體原因可以參考這篇文章。為什麼hashmap的初始化大小為2的冪次方

2.loadFactor。 載入因子。當hashmap的儲存容量達到了一定上限之後,如果還需要進行資料的儲存,則會利用載入因子對其進行擴容操作。一般而言,擴容大小為現在容量的0.75倍。舉個例子,假設現在的hashMap的初始化大小為16,但是現在由於容量已滿又要插入新的元素,所以先進行擴容操作,將容量擴充為16*0.75=12,也就是說擴大了12個容量。

3.threadshold: 擴容閥值。即擴容閥值 = HashMap總容量*載入因子。當hashMap的容量大於或者等於擴容閥值的時候就會去執行擴容。擴容的容量為當前HashMap總容量的兩倍。

這裡有一張網上找來的圖,來說明hashMap內部儲存原理。

hashMap

原始碼解析

我們在使用hashMap的時候,一般來說都是用put和get方法,所以我們分析原始碼,就從這兩個方法著手分析內部原理。

    public V put(K key, V value) {
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }
        if (key == null)
            return putForNullKey(value);
        int hash = sun.misc.Hashing.singleWordWangJenkinsHash(key);
        int i = indexFor(hash, table.length);
        for (HashMapEntry<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;
    }

複製程式碼

我們先來看看put方法。從程式碼可以看出,put方法主要做了這麼幾件事。

1.當我們在將key和value新增進入hashMap的時候,首先其會去判斷table是否為空(EMPTY_TABLE)。這裡需要說明下,這個table其實是一個陣列,我們前面提到過,hashmap內部其實是一個陣列來對資料進行儲存,所以這個table其實可以寫成table[ ]。當判斷這個table陣列為空的時候,他會去呼叫infalteTable()方法。而這個方法是做什麼的吶,我們在跳進去看看。

private void inflateTable(int toSize) {
        // Find a power of 2 >= toSize
        int capacity = roundUpToPowerOf2(toSize);
 
        // Android-changed: Replace usage of Math.min() here because this method is
        // called from the <clinit> of runtime, at which point the native libraries
        // needed by Float.* might not be loaded.
        float thresholdFloat = capacity * loadFactor;
        if (thresholdFloat > MAXIMUM_CAPACITY + 1) {
            thresholdFloat = MAXIMUM_CAPACITY + 1;
        }
 
        threshold = (int) thresholdFloat;
        table = new HashMapEntry[capacity];
    }

複製程式碼

可以看到,其實這個inflateTable方法是在對hashmap進行初始化容量操作。其初始化容量為capacity * loadFacctor。也就是我們前面說過的 初始化容量 * 載入因子。

2.之後hashmap回去判斷你儲存的key是否為空,if(key == null),如果為空,則呼叫putForNullKey()方法來進行空key的操作。這裡可以說是hashMap與hashTable的一個最大不同的地方,hashMap允許key為空,他有相應的處理key為空的操作方法,但是hashTable卻不能允許key為空,他沒有相應的操作方法。

3.之後對key進行一次hashcode的計算並且計算其index。緊接著遍歷整個table陣列,判斷是否有相同的key,如果發現有相同的key,則將key所攜帶的新的value替換掉之前舊的value,從而確保key的唯一性。之後進行addEntry方法中。

    void addEntry(int hash, K key, V value, int bucketIndex) {
        if ((size >= threshold) && (null != table[bucketIndex])) {
            resize(2 * table.length);
            hash = (null != key) ? sun.misc.Hashing.singleWordWangJenkinsHash(key) : 0;
            bucketIndex = indexFor(hash, table.length);
        }
 
        createEntry(hash, key, value, bucketIndex);
    }

複製程式碼

我們進入到addEntry方法中檢視。發現裡面會先對陣列需要儲存的大小和閥值進行一次比較,如果發現要儲存的已經超過了threshold閥值,那麼就要呼叫resize對其進行擴容操作。擴容的小大為2*table.length。之後從新計算hash,將結果儲存到bucket桶裡面。

那麼resize()方法中又做了那些操作吶?

    void resize(int newCapacity) {
        HashMapEntry[] oldTable = table;
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }
 
        HashMapEntry[] newTable = new HashMapEntry[newCapacity];
        transfer(newTable);
        table = newTable;
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }

複製程式碼

我們可以看到resize裡面僅僅只是初始化了一個新的更大的table陣列,並且把老的資料從新新增進入了新的table裡面去。

最後我們回到creatEntry方法中,檢視發現如果在bucket桶內發生了hash的碰撞,則將其轉化為連結串列的形式來進行儲存,不過在Java1.8之後會將其變為紅黑樹的形式儲存。在此將put方法原始碼分析完成。

我們再來看下get()方法。

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

複製程式碼

get方法一開始和put類似,都是先判斷key是否為空,如果為空,則呼叫相應的getForNullKey方法去進行處理。不為空,呼叫getEntry去進行查詢。我們再來看看getEntry裡面又做了什麼操作。

    final Entry<K,V> getEntry(Object key) {
        if (size == 0) {
            return null;
        }
 
        int hash = (key == null) ? 0 : sun.misc.Hashing.singleWordWangJenkinsHash(key);
        for (HashMapEntry<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;
    }

複製程式碼

我們可以看到,裡面也是先對key進行了一次hash操作,之後通過這個hash值來進行查詢,如果發現hash值相等,則再通過比較key的值來進行查詢,最終找到我們想要的e將其return返回,不然則返回為空,代表找不到此元素。

到此hashMap的整體原理講解完畢。

從原始碼的角度來談一談HashMap的內部實現原理

相關文章