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

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

HashMap概述

hashMap的底層實際上是基於一個數組來進行數據的存儲和取出。他繼承於Map這個接口來實現,經過put和get方法來操做數據的存和取。具體對於hashMap的使用,這裏不在具體舉例說明,使用起來並不困難。不過在談到HashMap的內部原理以前,咱們須要瞭解一下幾個名稱的意思。數組

1.initialCapacity。 這個翻譯爲初始化容量。爲hashMap的存儲的初始化空間的大小,咱們能夠經過構造方法來指定其大小,也能夠不指定採用 默認大小16。這裏須要說明一下,通常來講,容器的大小爲2的冪次方。至於爲何會是2的冪次方,具體緣由能夠參考這篇文章。爲何hashmap的初始化大小爲2的冪次方bash

2.loadFactor。 加載因子。當hashmap的存儲容量達到了必定上限以後,若是還須要進行數據的存儲,則會利用加載因子對其進行擴容操做。通常而言,擴容大小爲如今容量的0.75倍。舉個例子,假設如今的hashMap的初始化大小爲16,可是如今因爲容量已滿又要插入新的元素,因此先進行擴容操做,將容量擴充爲16*0.75=12,也就是說擴大了12個容量。源碼分析

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

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

hashMap

源碼解析

咱們在使用hashMap的時候,通常來講都是用put和get方法,因此咱們分析源碼,就從這兩個方法着手分析內部原理。spa

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方法主要作了這麼幾件事。.net

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。也就是咱們前面說過的 初始化容量 * 加載因子。code

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的總體原理講解完畢。

相關文章
相關標籤/搜索