Java面試 HashMap、HashSet源碼解析

本章所有源代碼基于JDK1.8版本

HashMap 和 HashSet 是 Java Collection Framework 的兩個(gè)重要成員,其中 HashMap 是 Map 接口的常用實(shí)現(xiàn)類,HashSet 是 Set 接口的常用實(shí)現(xiàn)類。雖然 HashMap 和 HashSet 實(shí)現(xiàn)的接口規(guī)范不同,但它們底層的 Hash 存儲(chǔ)機(jī)制完全一樣,甚至 HashSet 本身就采用 HashMap 來實(shí)現(xiàn)的,這一點(diǎn)我們通過查看HashSet的源代碼就能看得到。

通過閱讀源代碼分析存儲(chǔ)邏輯

HashMap

在日常的編程過程中,如果我們想要使用HashMap類,通常寫法如下:

HashMap<String, String> params = new HashMap<String, String>();
params.put("phone","1234567890");
params.put("dtype","json");

可以由此看出,HashMap中將一個(gè)key-value作為一個(gè)整體進(jìn)行處理,系統(tǒng)根據(jù)key的Hash值計(jì)算key-value的存儲(chǔ)位置,這樣可以保證快速存、取Map的key-value值。
下面我們查看一下HashMap中put方法的源碼實(shí)現(xiàn):

    //put方法的入口,直接調(diào)用putVal進(jìn)行處理
    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

    /**hash key計(jì)算得到的Hash值
     * key key
     * value key對應(yīng)的value
     * onlyIfAbsent 如果該參數(shù)為true,則不能修改已經(jīng)存在的值
     * 返回 key的hash值對應(yīng)的位置之前的value,如果沒有,直接返回空
     */
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        //如果長度為0,或者HashMap為空,則重新計(jì)算長度
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        //如果key為null,則hash值為0,i值為0,則獲取整個(gè)Node數(shù)組位置0處的值,是否為null
        //為null代表當(dāng)前位置0處,用于存放hash值為0位置沒有任何元素,則將現(xiàn)有key-value放入這個(gè)位置,作為鏈表第一個(gè)對象
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            //如果key不為空,且在隊(duì)列中這個(gè)key已經(jīng)存在,得到當(dāng)前位置node值。
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            //如果hash值對應(yīng)位置是一個(gè)鏈表數(shù)據(jù),則將當(dāng)前數(shù)據(jù)放在鏈表的最前,則將現(xiàn)有值放入鏈表中,
            //返回新增對象,此值如果已存在,則返回鏈表中這個(gè)值對應(yīng)的key-value
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                //否則循環(huán)判斷,將node放入節(jié)點(diǎn)中
                for (int binCount = 0; ; ++binCount) {
                    //如果當(dāng)前位置只有一個(gè)元素,則修改當(dāng)前位置為鏈表結(jié)構(gòu),將key-value放到鏈表最前
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    //如果hash值相同,且key值相同,不進(jìn)行處理,跳出處理程序
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    //下移一位,繼續(xù)判斷
                    p = e;
                }
            }
            //如果在上述的判斷中,一個(gè)是hash值找到的位置上已經(jīng)有元素,且key值相同,
            //或者找到位置已經(jīng)是鏈表數(shù)據(jù),獲取到key對應(yīng)的key-value節(jié)點(diǎn),則進(jìn)行如下判斷
            if (e != null) { // existing mapping for key
                V oldValue = e.value;//獲取當(dāng)前位置原有值
                //如果允許覆蓋修改,或者當(dāng)前位置原有的值為null,則直接修改當(dāng)前位置的值
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;//返回原有值
            }
        }
        ++modCount;
        if (++size > threshold)//如果HashMap大小已經(jīng)超過長度*加載因子,則重新計(jì)算長度
            resize();
        afterNodeInsertion(evict);
        return null;
    }

我們 用一張圖片來演示一下存儲(chǔ)過程


HashMap值存儲(chǔ)介紹
  • 當(dāng)put鍵值對[null, V]時(shí),計(jì)算null的hash值得知在數(shù)組中位置為0
    • 如果位置0處沒有任何對象,則直接將[null, V]存放在位置0處
    • 如果位置0處已有對象,則直接使用新的值V修改原有對象的值
  • 當(dāng)put鍵值對[S, V]時(shí),我們假設(shè)key值Ss的hash值相同,則在數(shù)組中對應(yīng)的位置相同
    我們比較已有值的key同新put的key進(jìn)行比較,不相等,則將新的值插入到原有數(shù)據(jù)之前,形成鏈表結(jié)構(gòu)
  • 當(dāng)put鍵值對[m, V]時(shí),如果計(jì)算key的hash值,得到數(shù)組中對應(yīng)位置上沒有元素,則直接將[m, V]放到位置上。
  • 當(dāng)put鍵值對[K, V]時(shí),原有位置已有元素,且key值相同,則直接使用新的值V覆蓋原有值
    上述代碼中有兩個(gè)變量很重要,分別為sizethreshold
  • size 已有對象數(shù)量
  • threshold 當(dāng)前HashMap所能容納對象的極限數(shù)量,值為當(dāng)前HashMap容量*負(fù)載因子,如果容量就回觸發(fā)重新設(shè)置HashMap大小的算法

HashMap提供三個(gè)構(gòu)造方法,分別為

  1. public HashMap() 生成長度默認(rèn)16,負(fù)載因子默認(rèn)為0.75的HashMap
  2. public HashMap(int initialCapacity) 生成長度為initialCapacity,負(fù)載因子默認(rèn)為0.75的HashMap
  3. public HashMap(int initialCapacity, float loadFactor) 生成長度為initialCapacity,負(fù)載因子為loadFactor的HashMap

構(gòu)造方法源碼為

    public HashMap(int initialCapacity, float loadFactor) {
        //如果設(shè)置的長度小于0,拋出異常
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        //如果長度大于可設(shè)置的最大長度`2的30次方,1073741824`,則長度為最大長度
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        //如果負(fù)載因子小于等于0,或者無法轉(zhuǎn)換為float類型,拋出異常
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        this.loadFactor = loadFactor;
        this.threshold = tableSizeFor(initialCapacity);
    }

HashMap中的每一個(gè)key-value都是一個(gè)對象,包含四個(gè)屬性,hash值,key,value,下一個(gè)元素

HashMap元素的獲取

程序中,獲取元素,可以調(diào)用HashMap的get(Object key)方法,邏輯為根據(jù)給定的key,得到key的hash值,然后使用hash值計(jì)算得到在數(shù)組中的位置。

  • 如果當(dāng)前位置沒有元素,直接返回null
  • 如果當(dāng)前位置有元素,且key值相同,直接返回當(dāng)前位置元素
  • 如果當(dāng)前位置為鏈表結(jié)構(gòu),則使用順序循環(huán)遍歷鏈表的方法從中查詢出key值相同的對象返回

HashMap總結(jié)

HashMap底層將key-value作為一個(gè)整體來進(jìn)行處理,起整體就是一個(gè)Node對象。本質(zhì)上就是Node的數(shù)組,存儲(chǔ)值時(shí)使用key的hash值尋址存儲(chǔ),讀取時(shí)根據(jù)key的hash值尋址讀取。因此,HashMap可以快速存取key-value,其實(shí)就是因?yàn)椴捎昧薻ey的hash值定位尋址的方式。
初始化HashMap時(shí),需要指定負(fù)載因子,負(fù)載因子值得就是HashMap中數(shù)值存儲(chǔ)的密度,密度越大,占用內(nèi)存越大,根據(jù)hash值進(jìn)行尋址花費(fèi)的時(shí)間也就越多,而存儲(chǔ)和讀取都需要使用尋址,因此會(huì)降低運(yùn)行效率。如果負(fù)載因子設(shè)置過小,會(huì)使得密度降低,造成內(nèi)存空間浪費(fèi)。一般來說,我們無需修改HashMap的負(fù)載因子。
從源代碼中我們也可以看出,如果數(shù)組中已有對象個(gè)數(shù)超過最大承受極限,也就是數(shù)組長度*負(fù)載因子,則需要重新計(jì)算數(shù)組長度,這個(gè)過程會(huì)進(jìn)行數(shù)組內(nèi)已有所有元素的重新計(jì)算位置和拷貝,造成不必要的系統(tǒng)開銷。因此如果我們可以提前知道HashMap中元素的最大數(shù)量,可以根據(jù)需要設(shè)置到合適大小,避免重新計(jì)算。

HashSet

通過閱讀HashSet的源代碼,發(fā)現(xiàn)HashSet類的內(nèi)部就是使用HashMap。HashSet的構(gòu)造函數(shù)為:

    public HashSet() {
        map = new HashMap<>();
    }

因此在HashSet內(nèi)部也是使用Hash值計(jì)算的方式?jīng)Q定集合元素的存放位置
這點(diǎn)可以從HashSet 和 HashMap 之間有很多相似之處,對于 HashSet 而言,系統(tǒng)采用 Hash決定集合元素的存儲(chǔ)位置,這樣可以保證能快速存、取集合元素;

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

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

  • 實(shí)際上,HashSet 和 HashMap 之間有很多相似之處,對于 HashSet 而言,系統(tǒng)采用 Hash 算...
    曹振華閱讀 2,561評論 1 37
  • HashMap 是 Java 面試必考的知識點(diǎn),面試官從這個(gè)小知識點(diǎn)就可以了解我們對 Java 基礎(chǔ)的掌握程度。網(wǎng)...
    野狗子嗷嗷嗷閱讀 6,810評論 9 107
  • 前言 這次我和大家一起學(xué)習(xí)HashMap,HashMap我們在工作中經(jīng)常會(huì)使用,而且面試中也很頻繁會(huì)問到,因?yàn)樗?..
    liangzzz閱讀 8,119評論 7 102
  • 呵呵
    最小二乘法閱讀 171評論 0 0
  • 嗨,老友。 剛剛看完你寫的《與老友說》。 以前,我們都是寫信,享受見字如面的感覺。如今,信件不易寄送。我們在簡書彼...
    小飛俠303閱讀 417評論 4 6

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