Java集合-HashMap詳解

一、HashMap的結(jié)構(gòu)及其原理

1.什么是HashMap

HashMap是基于哈希表的Map接口的非同步實現(xiàn)。是雙列集合,一次儲存鍵與值鍵與值一一對應(yīng)(鍵是唯一性,值不唯一,同時鍵與值都可以是null)。

2.數(shù)據(jù)結(jié)構(gòu)

Java中最基本的數(shù)據(jù)結(jié)構(gòu)是數(shù)組和引用(指針),HashMap通過這兩個數(shù)據(jù)結(jié)構(gòu)實現(xiàn)。HashMap是一個==“鏈表散列”==的數(shù)據(jù)接口,即通過數(shù)組+鏈表結(jié)合實現(xiàn)。而從jdk1.8起,為了進(jìn)一步提高效率,引入了紅黑樹,即數(shù)組+鏈表/紅黑樹。(當(dāng)同一hash索引下的節(jié)點個數(shù)超過8個的話,就通過紅黑樹的形式存儲起來)

HashMap結(jié)構(gòu)圖.png

為什么要采取這種結(jié)構(gòu)呢?
這里我們簡單的說一下,不做深究:
1.數(shù)組的查找通過索引查找是非??斓模瑫r間復(fù)雜度o(1)。
2.鏈表呢,我們刪除和增加效率是非常高的。
3.紅黑樹,將普通鏈表的查找速度由o(n)降低到了o(logn)

所以這幾者相結(jié)合,將會達(dá)到很好的效果,具體如何結(jié)合的,我們通過下面的分析一步一步探究。


3.通過源碼一步一步研究HashMap的存儲元素過程

去看了源碼,密密麻麻的好多代碼啊,難道要一行一行的去研究嗎?
不用,研究他們的核心部分即可,那從何處開始研究呢?
我們就通過我們使用HashMap的步驟去研究:

1.HashMap map = new HashMap();

附上這行代碼的源碼:

/**
     * 哈希表的加載因子
     * The load factor for the hash table.
     *
     * @serial
     */
    final float loadFactor;
/**  
     * 在構(gòu)造函數(shù)未指定的時候,加載因子的默認(rèn)值=0.75f
     * The load factor used when none specified in constructor.
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted 其他的屬性都是默認(rèn)值
    }

這里我們要記住的就是loadFactor(加載因子),那么這個加載因子有什么用呢?
這個加載因子是和數(shù)組的空間利用率有關(guān),即一個數(shù)組的長度為length時,我們所能存儲元素的長度只能是length*loadFactor

2.public V put(K key, V value),key代表鍵,value代表值
看這個方法的源代碼:

public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
//繼續(xù)解刨,看putVal(hash(key),key,value,false,true)
//首先我們看看hash(key)干嘛的
static final int hash(Object key) {
        int h;
        /*
            我們看到,當(dāng)key為空的時候,會返回值0,不為空的時候,會將key的哈希值的高位(16位)與低位(16位)進(jìn)行^(異或)計算,然后返回結(jié)果值,這個結(jié)果有什么用呢,是用來用于后面再次計算得出所在數(shù)組中的索引值
        */
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
//這個就是儲存每個節(jié)點的具體步驟了
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        /*
            變量名解釋:tab(數(shù)組) p(節(jié)點,數(shù)組中每個元素)
            n為數(shù)組的長度,i為數(shù)組的索引
        */
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        //如果tab引用為空指針,或者數(shù)組長度為0,即返回一個新的數(shù)組,長度為n
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        /*
            這里有個(n-1)&hash式子,計算的結(jié)果是數(shù)組的索引
            這里的邏輯是判斷數(shù)組中這個索引的對應(yīng)的值是否為null,若是
            為空的話,將將這個節(jié)點儲存在這個索引的位置
        */
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {//執(zhí)行這里,說明這個索引位置儲存的不是鏈表就是紅黑樹了
            Node<K,V> e; K k;
            /*
            這里的邏輯是,這個索引位置已有節(jié)點了,那么就判斷添加的的key
            和這里已有節(jié)點的key是否是同一個地址的或者是否是內(nèi)容相同的,是的話,就將添加進(jìn)來的value值替換掉原有節(jié)點的value值
            */
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            //判斷當(dāng)前索引位置的對象是否屬于紅黑樹,是的話,添加上去
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {//執(zhí)行這里說明這里不是紅黑樹,是鏈表
                /*
                    遍歷循環(huán)鏈表
                */
                for (int binCount = 0; ; ++binCount) {
                    //此節(jié)點的下一個節(jié)點是否為空節(jié)點,是的話,直接賦值節(jié)點
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                            //判斷此位置的鏈表個數(shù)是否超過了8個,超過
                            //將執(zhí)行teeifyBin()方法(擴(kuò)容或轉(zhuǎn)為紅黑樹),并結(jié)束并推出循環(huán)
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    //節(jié)點不為空,判斷哈希值和key是否是一樣的,是的話結(jié)束并退出循環(huán)
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;//遍歷下一個節(jié)點
                }
            }
            //判斷添加的節(jié)點是否為新節(jié)點,不是新節(jié)點,則將新的value值替換老的value值,并返回老的value值
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        //記錄HashMap結(jié)構(gòu)修改的個數(shù)(只有添加了新的節(jié)點)
        ++modCount;
        //添加元素后此時數(shù)組中元素個數(shù)>數(shù)組中規(guī)定的個數(shù)(數(shù)組長度*負(fù)載因子)時擴(kuò)容
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

這里我們通過源碼分析了我們使用HashMap添加對象的一系列過程,這里我們畫個圖看看一比較形象的邏輯:


HashMap添加元素邏輯圖.png

通過分析put(K key,V value)的源碼,我們知道了HashMap內(nèi)部結(jié)構(gòu):

  • 數(shù)組Node<K key,V value>:
    //節(jié)點,通過內(nèi)部內(nèi)實現(xiàn)
    static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;//存放hash值(key的)
        final K key; //存放key值
        V value;  //存放value至
        Node<K,V> next; //存放下一個節(jié)點的地址

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

上面就是鏈表的組成部分,一個一個節(jié)點組成的。而數(shù)組呢,就是存放每一個hash值相同的節(jié)點,從第一個開始存放,往后相同的hash值節(jié)點便通過鏈表的形式存放。

  • hash算法:
    hash算法是我們需要了解的部分,因為你存入的元素放在數(shù)組的那個位置是通過這個來計算的,即上面出現(xiàn)的計算公式:
    (n-1)&hash
    計算出索引位置的算法.png

具體計算過程如圖所示。(這里需要注意的是,因為數(shù)組的長度始終是2的次方冪,這里的與運(yùn)算,就相當(dāng)于給length-1取模運(yùn)算)

注意:通過這樣的算法,我們其實可以知道,總會有一種情況,計算得出的值是相同的,所以需要解決哈希沖突,HashMap的解決辦法就是一開始我們介紹的形式,數(shù)組+鏈表,即==鏈表法==。還有一種方法是==開放地址法==(即通過探測算法,當(dāng)此位置被占用,即尋找下一個可用的位置)。

二.HashMap的哈希表散列運(yùn)算

我們知道每一個對象都有它的哈希值,這是Object類中的本地方法,具體怎么計算的,目前我也不知道,只要知道這個對于每一個對象得出的值基本不可能即可。
因為通過%(取模運(yùn)算)是要進(jìn)行/(除法)運(yùn)算的,效率低。(其實到底低多少我也不知道)
而通過&(與運(yùn)算)是直接通過對二進(jìn)制來計算的,效率高。
那么:
(n-1)&hash
n為什么要-1呢?
首先,我們要知道n是數(shù)組的長度,而HashMap的數(shù)組長度是==偶數(shù),即長度為2的冪次方,每次擴(kuò)容都是之前的一倍==。
然后我們知道偶數(shù)的二進(jìn)制最低位是0,那好,偶數(shù)的與運(yùn)算我們可以很明確的得出,結(jié)果必然是偶數(shù),所以,如果直接用偶數(shù)來進(jìn)行哈希表散列將浪費(fèi)一半的空間。
所以,我們將n-1得到奇數(shù),而奇數(shù)進(jìn)行與運(yùn)算,得到的結(jié)果可能是可能是偶數(shù),也可能是奇數(shù),這樣就不會浪費(fèi)空間了!

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

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