圖解HashMap

什么是HashMap,文章內(nèi)HashMap源碼主要來自Android 7.0

HashMap是開發(fā)中常用的一個類,那么他究竟是什么呢?

HashMap是一個存儲key-value的集合,底層實現(xiàn)的是數(shù)組,所以可以看作HashMap是對數(shù)組的一種封裝。

構(gòu)造方法

HashMap構(gòu)造函數(shù).png

HashMap構(gòu)造函數(shù).png

不管調(diào)用的是哪一個方法, 最終都會回調(diào)兩個參數(shù)的這個構(gòu)造函數(shù),第一個參數(shù)是容量,第二個參數(shù)是閾值(用于擴容的時候計算容量)

先看看HashMap主要的成員變量

  /**
     * HashMap默認容量
     */
    static final int DEFAULT_INITIAL_CAPACITY = 4;

    /**
     * HashMap最大可存儲的容量值  1<<30
     */
    static final int MAXIMUM_CAPACITY = 1 << 30;

    /**
     * 加載因子(閾值)如果put進來的元素數(shù)量>=總數(shù)量*0.75的時候, 就會進行擴容了
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    /**
     * EMPTY_TABLE 看了一下,好像沒啥用。。。
     */
    static final HashMapEntry<?,?>[] EMPTY_TABLE = {};
    transient HashMapEntry<K,V>[] table = (HashMapEntry<K,V>[]) EMPTY_TABLE;

    /**
     * 這個size表示容量值,put了幾次,這個size就是幾,所以我們方法中用的size() 就是返回的這個值
     */
    transient int size;

因為HashMap常用的就是get和put,所以主要分析一下這兩個方法,在講這個之前,先看一下HashMapEntry這個類吧

HashMapEntry

HashMapEntry繼承自Map.Entry

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

HashMapEntry的結(jié)構(gòu)是鏈表(在api25之前是鏈表,在api26開始引入了紅黑樹, 當節(jié)點>8個的時候會轉(zhuǎn)為紅黑樹, 節(jié)點<6個的時候又會轉(zhuǎn)回為鏈表, 紅黑樹跳這里HashMap在Api26后的應用---紅黑樹篇),所以存儲數(shù)據(jù)的時候是這樣的

存儲結(jié)構(gòu).png

關(guān)于鏈表可參考其他文章

現(xiàn)在來講一講HashMap的put和get

put

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的方法并不長,首次進來時會判斷table是不是EMPTY_TABLE,就是上面那兩數(shù)組,然后會執(zhí)行inflatetable方法,這個方法就不看了。。。只有第一次put時候才會進入,因為只有那個時候table==EMPTY_TABLE,在inflatetable里,table就會被重新賦值
接下來看第二個判斷 key==null
看看這個方法putForNullKey()

 private V putForNullKey(V value) {
        for (HashMapEntry<K,V> e = table[0]; e != null; e = e.next) {
            if (e.key == null) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
        modCount++;
        addEntry(0, null, value, 0);
        return null;
    }

如果已經(jīng)有了一個 key為null的元素,那么就會替換他的value值,所以HashMap只能由一個空key。
sun.misc.Hashing.singleWordWangJenkinsHash(key);這個方法就是根據(jù)key計算hash值,然后通過indexFor方法算出key在table中的下標。由于數(shù)組的存儲方式大概是這樣的

image.png

但是由于下標是根據(jù)key的hash和數(shù)組長度計算來的,所以有可能下標會一樣,這個時候HashMapEntry這個鏈表的用處就體現(xiàn)出來了,如果下標一樣的時候,那么就會比對HashMapEntry的key值是否一致,如果一致,就替換原key-value,如果沒有與新添加的key一致的值,就會在HashMapEntry中新加一個節(jié)點,所以現(xiàn)在的存儲方式變成了這樣

hashmap存儲方式.png

如果是替換就value,會直接吧舊的value返回回去,如果不是的話就會走addEntry方法, 這個方法有三個作用

  • 擴容
  • 拷貝數(shù)據(jù)
  • 插入新數(shù)據(jù)
    跟進一下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);
    }

首先判斷的是size是否大于閾值(總?cè)萘?0.75),并且table[bucketIndex]!=null, 所以只有兩個條件成立的時候才會進行擴容

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);
    }

newCapacity的大小等于就數(shù)組長度*2, 所以下方構(gòu)建的newTable的長度就是原數(shù)組的長度兩倍,到這里,就進行擴容完畢了,但是新數(shù)組是有了,但是沒數(shù)據(jù)??!不急,看transfer方法

transfer()

void transfer(HashMapEntry[] newTable) {
        int newCapacity = newTable.length;
        for (HashMapEntry<K,V> e : table) {
            while(null != e) {
                HashMapEntry<K,V> next = e.next;
                int i = indexFor(e.hash, newCapacity);
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            }
        }
    }

看到了吧,或進行一個雙層循環(huán),先循環(huán)數(shù)組,然后在循環(huán)里面節(jié)點,直到next==null的時候,會跳出當前循環(huán),進行下一次循環(huán),直到循環(huán)完畢,也就是新數(shù)據(jù)賦值完畢
再回到resize方法,再看下面的代碼,把新數(shù)組newTable又給了table,threshold又得到了擴容后新的閾值,到這一步,擴容和拷貝數(shù)據(jù)就已經(jīng)完成了。
再回看addEntry方法,又會更具新數(shù)組的大小和key的hash值重新計算下標,傳遞給createEntry(hash, key, value, bucketIndex)方法中,

 void createEntry(int hash, K key, V value, int bucketIndex) {
        HashMapEntry<K,V> e = table[bucketIndex];
        table[bucketIndex] = new HashMapEntry<>(hash, key, value, e);
        size++;
    }

到此,hashmap的put就結(jié)束了,回頭看看。。。其實還算蠻簡單的哈


毛骨悚然.png

那么get方法呢?

get

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;
    }

get方法最終會調(diào)用這個getEntry方法,看看里面的方法是不是很眼熟,計算hash,比對key。



對!就是這么簡單,同樣也是根據(jù)hash和數(shù)組長度獲取下標,然后就是這么一個循環(huán),只要hash值一樣并且key有一樣的就會返回這個元素,否則就是返回null

總結(jié)一下:
put添加元素的操作為:

計算key的hash ==> 根據(jù)hash和數(shù)組長度計算對應的數(shù)組下標 ==> 如果當前下標內(nèi)容為null,就直接添加,否則的話會進入一個循環(huán),在這個循環(huán)中去尋找鏈表內(nèi)有沒有當前key值,有的話替換原value,沒有的話插入到最后一個節(jié)點

put步驟.png

get獲取元素

計算key的hash ==> 根據(jù)hash和數(shù)組長度計算對應的數(shù)組下標 ==> 如果當前下標元素不為null,進入循環(huán),在這個循環(huán)中去尋找鏈表內(nèi)有沒有當前key值,有的話返回,沒有的話就返回null
get就不畫了啊 自行體會


話說你們畫圖都用啥啊。。。 我這大晚上的用截圖工具扣扣畫畫好累,win10自帶的畫圖工具感覺用不來

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

  • 1. 前言 本文的源碼是基于JDK1.7,JDK1.8中HashMap的實現(xiàn),引入了紅黑樹,在后面的文章會寫到。后...
    唐江旭閱讀 40,159評論 16 175
  • HashMap 是 Java 面試必考的知識點,面試官從這個小知識點就可以了解我們對 Java 基礎的掌握程度。網(wǎng)...
    野狗子嗷嗷嗷閱讀 6,817評論 9 107
  • 在看《紅高粱家族》與它的電視劇
    自動控制原理要加油閱讀 233評論 0 1
  • 這里是烏鎮(zhèn),一個夢里的水鄉(xiāng),一個來過就不曾離開的地方。 我在這里,等你。 你若能來,我必生死相依。
    夏夏大小姐閱讀 136評論 0 0
  • Hello World
    九袋大弟子閱讀 196評論 0 0

友情鏈接更多精彩內(nèi)容