HashMap 源碼分析(一)

一、概述

在Java開(kāi)發(fā)中,HashMap使用的頻率還是比較多的,主要通過(guò)key-value的形式來(lái)存儲(chǔ)數(shù)據(jù),但是HashMap的原理是怎樣的呢?他是如何進(jìn)行增刪改查的呢?所以今天我們就帶著這些問(wèn)題來(lái)看源碼,主要看它的put,get,remove方法。本文我們使用的JDK版本為1.8。

二、源碼分析

在分析源碼之前,我們先來(lái)看一下HashMap的簡(jiǎn)單使用:

public static void main(String[] args) {
        HashMap hashMap = new HashMap();
        hashMap.put("key","張三");
        System.out.println(hashMap.get("key"));
}

我們按照這個(gè)順序來(lái)繼續(xù)分析源碼,首先來(lái)看一下四個(gè)構(gòu)造方法:相關(guān)解釋已經(jīng)寫(xiě)在了注釋中。


    構(gòu)造一個(gè)空的HashMap,默認(rèn)的初始容量為:16,默認(rèn)的加載因子為:0.75。
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }

   構(gòu)造一個(gè)空的 指定容量的HashMap,默認(rèn)的加載因子為:0.75。
    public HashMap(int initialCapacity) {
        //繼續(xù)調(diào)用了第三個(gè)調(diào)用方法
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

    構(gòu)造一個(gè)空的HashMap,指定初始容量,和 加載因子。
    public HashMap(int initialCapacity, float loadFactor) {
        //一些驗(yàn)證的操作
        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);
        //設(shè)置加載因子和初始容量
        this.loadFactor = loadFactor;
        this.threshold = tableSizeFor(initialCapacity);
    }

    構(gòu)造一個(gè)與指定Map相同映射的HashMap 可以理解為將Map轉(zhuǎn)為HashMap
    public HashMap(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
    }


二、增和改

現(xiàn)在我們帶著第一個(gè)問(wèn)題:HashMap是如何插入數(shù)據(jù)的呢?我們直接來(lái)看put方法:

    @param key 鍵
    @param value 值   
    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

可以看到HashMap的put方法又調(diào)用了putVal方法,第一個(gè)參數(shù)中又調(diào)用了hash(key),我們先來(lái)看看hash(key的作用):這里問(wèn)題,為什么進(jìn)行位運(yùn)算

    static final int hash(Object key) {
        int h;
        //如果key為空,返回0,否則返回key.hashCode的異或運(yùn)算 位移16位
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

這里簡(jiǎn)單舉例一個(gè)異或運(yùn)算:比如
h=2987074,在位運(yùn)算中都是通過(guò)二進(jìn)制進(jìn)行,
h的二進(jìn)制:0010 1101 1001 0100 0100 0010
h位移16位之后的二進(jìn)制:0000 0000 0000 0000 0010 1101
這兩個(gè)二進(jìn)制數(shù)再進(jìn)行^(異或)運(yùn)算,
結(jié)果為:0010 1101 1001 0100 0110 1111,轉(zhuǎn)為十進(jìn)制為:2987119。

看完hash方法之后繼續(xù)看putVal方法,但是在看putVal方法之前,我們先了解一下HashMap的一個(gè)內(nèi)部類,Node。

在Node類中,存儲(chǔ)了hash,key,value,和next,next就是當(dāng)前node指向的下一個(gè)節(jié)點(diǎn),這就是一個(gè)鏈表結(jié)構(gòu)。

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

        Node(int hash, K key, V value, Node<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }

        public final K getKey()        { return key; }
        public final V getValue()      { return value; }
        public final String toString() { return key + "=" + value; }

        public final int hashCode() {
            return Objects.hashCode(key) ^ Objects.hashCode(value);
        }

        public final V setValue(V newValue) {
            V oldValue = value;
            value = newValue;
            return oldValue;
        }

        public final boolean equals(Object o) {
            if (o == this)
                return true;
            if (o instanceof Map.Entry) {
                Map.Entry<?,?> e = (Map.Entry<?,?>)o;
                if (Objects.equals(key, e.getKey()) &&
                    Objects.equals(value, e.getValue()))
                    return true;
            }
            return false;
        }
    }

繼續(xù)來(lái)看putVal方法:

    /**
     * @param hash key 的hash
     * @param key 
     * @param value
     * @param onlyIfAbsent 如果為true,則不更改現(xiàn)有值
     * @param evict 如果為false,則表處于創(chuàng)建模式
     * @return valur or null
     */
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab;
        Node<K,V> p;
        int n, i;
        //如果table為空,或 長(zhǎng)度為0時(shí)
        //這里的table為Node<K,V>[] table,是一個(gè)數(shù)組,Node相當(dāng)于鏈表,所以到這里我們可以知道HashMap
        //底層使用的是數(shù)組+鏈表
        if ((tab = table) == null || (n = tab.length) == 0)
            //n = 創(chuàng)建一個(gè)新的tab并返回長(zhǎng)度
            //這里的n就是tab的長(zhǎng)度,即Node<K,V>[]數(shù)組長(zhǎng)度
            n = (tab = resize()).length;
        //通過(guò)數(shù)組長(zhǎng)度-1 與 hasn進(jìn)行與運(yùn)算拿到i即下標(biāo),再通過(guò)下標(biāo)取數(shù)組中的node,
        //如果為null是,創(chuàng)建一個(gè)新的node,賦值給tab[i]
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; 
            K k;
            //如果p中和傳入的hash相等,與 key相等時(shí),
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                //將p即node,賦值給e
                e = p;
            //否則 p是一個(gè)樹(shù)節(jié)點(diǎn),將其插入到樹(shù)中,并賦值給e
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                for (int binCount = 0; ; ++binCount) {
                    //如果p的下一個(gè)節(jié)點(diǎn)為空,則創(chuàng)建一個(gè)node,賦值給p.next,
                    //這里采用的是尾插法,
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        //如果數(shù)量大于 TREEIFY_THRESHOLD 即8時(shí),將鏈表Node,轉(zhuǎn)換為樹(shù)(紅黑樹(shù))
                        //到這里 HashMap的結(jié)構(gòu)就成為了數(shù)組+鏈表+紅黑樹(shù)。
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    //如果hash相等,則直接跳出循環(huán),
                    //到這里就表示當(dāng)前的e(node)和傳入的值已經(jīng)相等了,所以沒(méi)必要循環(huán)了
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            //如果node不為空,保存value為oldValue,
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                //如果需要改變現(xiàn)有值,value為空時(shí),將傳入的value賦值給node.value
                //這里對(duì)應(yīng)的就是修改key的value
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        //如果當(dāng)前長(zhǎng)度大于默認(rèn)長(zhǎng)度
        //++size 就是每次put后size+1,getSize()就是這個(gè)size
        if (++size > threshold)
            //執(zhí)行擴(kuò)容
            resize();
        afterNodeInsertion(evict);
        return null;
    }


三、查

put方法已經(jīng)分析完成了,相關(guān)的注釋我已經(jīng)寫(xiě)在了上面,大家可以認(rèn)真看,其實(shí)并不太難,接下來(lái)我們來(lái)看下一個(gè)問(wèn)題,HashMap中是如何通過(guò)key獲取Value的呢?接下來(lái)繼續(xù)看get方法:

public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
 }
final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab;
        Node<K,V> first, e;
        int n;
        K k;
        //如果tab不為null,與 查找到的node不為空時(shí),
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            //如果第一個(gè)node hash 和key和我們想要的hash key相同,直接返回此node
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            //如果node的next不為空,
            if ((e = first.next) != null) {
                //如果node時(shí)TreeNode(樹(shù)節(jié)點(diǎn))
                if (first instanceof TreeNode)
                    //從樹(shù)中查找
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                //通過(guò)循環(huán) 遍歷節(jié)點(diǎn)的next,如果找到hash和key相同的則返回。
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        //沒(méi)有找到 返回null
        return null;
    }


四、刪

到這里,增改查就已經(jīng)分析完畢,最后來(lái)看刪除的方法:remove();

    public V remove(Object key) {
        Node<K,V> e;
        //通過(guò)removeNode方法查找對(duì)應(yīng)的Node節(jié)點(diǎn)返回node.value
        return (e = removeNode(hash(key), key, null, false, true)) == null ?
            null : e.value;
    }

在removeNode方法中,和getNode原理大致相同,我們來(lái)簡(jiǎn)單看一下:

final Node<K,V> removeNode(int hash, Object key, Object value,
                               boolean matchValue, boolean movable) {
        Node<K,V>[] tab;
        Node<K,V> p;
        int n, index; //n為長(zhǎng)度,index為下標(biāo)
        //如果tab不為空 與 查找到的node不為空時(shí),
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (p = tab[index = (n - 1) & hash]) != null) {
            Node<K,V> node = null, e; K k; V v;
            //如果node中的hash和傳入的hash相等,且key相等
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                //將p賦值給node
                node = p;
            //如果next不為空時(shí),
            else if ((e = p.next) != null) {
                //如果p是樹(shù)節(jié)點(diǎn)
                if (p instanceof TreeNode)
                    //node = 從樹(shù)節(jié)點(diǎn)中查找
                    node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
                else {
                    //否則 遍歷table,知道找到要?jiǎng)h除的node
                    do {
                        if (e.hash == hash &&
                            ((k = e.key) == key ||
                             (key != null && key.equals(k)))) {
                            node = e;
                            break;
                        }
                        p = e;
                    } while ((e = e.next) != null);
                }
            }
            //如果node不為空,與 值相等時(shí)
            if (node != null && (!matchValue || (v = node.value) == value ||
                                 (value != null && value.equals(v)))) {
                //如果node時(shí)樹(shù)節(jié)點(diǎn),就從樹(shù)節(jié)點(diǎn)中刪除。
                if (node instanceof TreeNode)
                    ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
                //更改node的next為要?jiǎng)h除的next,就是更改當(dāng)前node的next引用 為 要?jiǎng)h除的next的引用
                //這樣就相當(dāng)于與要?jiǎng)h除的節(jié)點(diǎn)斷開(kāi)了連接 從而實(shí)現(xiàn)了刪除
                else if (node == p)
                    tab[index] = node.next;
                else
                    p.next = node.next;
                ++modCount;
                //size-1
                --size;
                afterNodeRemoval(node);
                //最后返回刪除的node
                return node;
            }
        }
        return null;
    }

到這里HashMap的增刪改查的源碼我們已經(jīng)看完了,所對(duì)應(yīng)的為:put(),get(),remove()。在上面的代碼中,我都已經(jīng)寫(xiě)好了注釋,根據(jù)注釋結(jié)合代碼看還是非常通俗易懂的。

五、總結(jié)

通過(guò)上面的分析,我們已經(jīng)基本理解了HashMap增刪改查的大致流程了,我們總結(jié)如下:

  1. 增(put):

①:通過(guò)key的hashcode和hashcode的位移16位進(jìn)行異或運(yùn)算得到一個(gè)hash值。
②:如果存儲(chǔ)節(jié)點(diǎn)(Node)的數(shù)組為空,則創(chuàng)建一個(gè)新的數(shù)組。
③:通過(guò)數(shù)組的長(zhǎng)度-1 和 上面得到的hash進(jìn)行&運(yùn)算,得到一個(gè)node即p;
④:如果p為空,說(shuō)明該位置沒(méi)有存儲(chǔ)節(jié)點(diǎn),則創(chuàng)建一個(gè)node對(duì)象,并存入對(duì)應(yīng)的下標(biāo)中。
⑤:否則,p不為空時(shí),判斷Node是否是TreeNode(樹(shù)節(jié)點(diǎn)),如果是則插入到樹(shù)節(jié)點(diǎn)。
⑥:如果不是樹(shù)節(jié)點(diǎn),則遍歷鏈表,直到鏈表的next為空時(shí),將此node賦值給next。
⑦:同時(shí)判斷遍歷的次數(shù),如果大于等于TREEIFY_THRESHOLD即8-1時(shí),將鏈表轉(zhuǎn)換為紅黑樹(shù)。
⑧:如果key hash相同,且需要修改value時(shí),則修改value。
⑨:最后進(jìn)行收尾工作,++size,如果長(zhǎng)度大于threshold,進(jìn)行擴(kuò)容。

  1. 改:

其實(shí)改的方法是包含在put方法之中的,當(dāng)傳入的key相同時(shí),則根據(jù)參數(shù)onlyIfAbsent來(lái)決定是否更新這個(gè)值。

  1. 查(get):

①:判斷tab不為空時(shí),算出數(shù)組中的index,找到頭節(jié)點(diǎn)node。
②:如果頭節(jié)點(diǎn)的key和hash與傳入的相同,直接返回此node。
③:如果沒(méi)找到,繼續(xù)判斷,如果node為紅黑樹(shù)結(jié)構(gòu),則從紅黑樹(shù)中查找。
④:如果沒(méi)找到,遍歷鏈表,繼續(xù)查找key hash和傳入相同的node,找到則返回。
⑤:沒(méi)找到,返回null。

  1. 刪(remove):

查和刪的原理大致相同,首先都是通過(guò)一系列判斷查找出對(duì)應(yīng)的node節(jié)點(diǎn),最后通過(guò)更改next的指向來(lái)達(dá)到刪除的效果。其中有一個(gè)參數(shù):matchValue,如果為true時(shí),則只會(huì)在兩個(gè)value相等的情況下才會(huì)刪除。

最后上一張HashMap的結(jié)構(gòu)圖,以供大家理解。


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

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

  • 一、Map概述 首先,在Java集合中,Map是一種以key-value鍵值對(duì)形式保存存儲(chǔ)數(shù)據(jù)的特殊集合類。而Ha...
    LeonardoEzio閱讀 780評(píng)論 3 18
  • HashMap源碼分析——put和get(二) 鏈接 上一節(jié) :HashMap源碼分析——put和get(一) 2...
    施瓦閱讀 596評(píng)論 0 1
  • 1.HashMap的底層實(shí)現(xiàn)圖示 如上圖所示: HashMap底層是由數(shù)組+(鏈表)=(紅黑樹(shù))組成,每個(gè)存儲(chǔ)在H...
    Bamboo_a67a閱讀 350評(píng)論 0 0
  • 什么是HashMap HashMap是一個(gè)用于存儲(chǔ)Key-Value鍵值對(duì)的集合,也是Java中使用頻率最高的用于...
    言淺閱讀 352評(píng)論 0 0
  • 一、寫(xiě)在前面 相信讀者也看過(guò)了不少講解 HashMap 源碼的文章了,筆者認(rèn)為,一切脫離源碼去講原理的都是泛泛而談...
    CoderZS閱讀 402評(píng)論 1 1

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