欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

深入理解Java中的HashMap

 更新時(shí)間:2021年06月10日 17:57:04   作者:CoderSheeper  
HashMap是Java程序員使用頻率最高的用于映射(鍵值對(duì))處理的數(shù)據(jù)類(lèi)型。隨著JDK(Java Developmet Kit)版本的更新,JDK1.8對(duì)HashMap底層的實(shí)現(xiàn)進(jìn)行了優(yōu)化,例如引入紅黑樹(shù)的數(shù)據(jù)結(jié)構(gòu)和擴(kuò)容的優(yōu)化等。本文將深入探討HashMap的結(jié)構(gòu)實(shí)現(xiàn)和功能原理

一、HashMap的結(jié)構(gòu)圖示

​本文主要說(shuō)的是jdk1.8版本中的實(shí)現(xiàn)。而1.8中HashMap是數(shù)組+鏈表+紅黑樹(shù)實(shí)現(xiàn)的,大概如下圖所示。后面還是主要介紹Hash Map中主要的一些成員以及方法原理。

​那么上述圖示中的結(jié)點(diǎn)Node具體類(lèi)型是什么,源碼如下。Node是HashMap的內(nèi)部類(lèi),實(shí)現(xiàn)了Map.Entery接口,主要就是存放我們put方法所添加的元素。其中的next就表示這可以構(gòu)成一個(gè)單向鏈表,這主要是通過(guò)鏈地址法解決發(fā)生hash沖突問(wèn)題。而當(dāng)桶中的元素個(gè)數(shù)超過(guò)閾值的時(shí)候就換轉(zhuǎn)為紅黑樹(shù)。

//hash桶中的結(jié)點(diǎn)Node,實(shí)現(xiàn)了Map.Entry
static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    V value;
    Node<K,V> next; //鏈表的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; }
    //重寫(xiě)Object的hashCode
    public final int hashCode() {
        return Objects.hashCode(key) ^ Objects.hashCode(value);
    }
    public final V setValue(V newValue) {
        V oldValue = value;
        value = newValue;
        return oldValue;
    }
    //equals方法
    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;
    }
}
//轉(zhuǎn)變?yōu)榧t黑樹(shù)后的結(jié)點(diǎn)類(lèi)
static final class TreeNode<k,v> extends LinkedHashMap.Entry<k,v> {
    TreeNode<k,v> parent;  // 父節(jié)點(diǎn)
    TreeNode<k,v> left; //左子樹(shù)
    TreeNode<k,v> right;//右子樹(shù)
    TreeNode<k,v> prev;    // needed to unlink next upon deletion
    boolean red;    //顏色屬性
    TreeNode(int hash, K key, V val, Node<k,v> next) {
        super(hash, key, val, next);
    }
    //返回當(dāng)前節(jié)點(diǎn)的根節(jié)點(diǎn)
    final TreeNode<k,v> root() {
        for (TreeNode<k,v> r = this, p;;) {
            if ((p = r.parent) == null)
                return r;
            r = p;
        }
    }
}

​上面只是大概了解了一下HashMap的簡(jiǎn)單組成,下面主要介紹其中的一些參數(shù)和重要的方法原理實(shí)現(xiàn)。

二、HashMap的成員變量以及含義

//默認(rèn)初始化容量初始化=16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
//最大容量 = 1 << 30
static final int MAXIMUM_CAPACITY = 1 << 30;
//默認(rèn)加載因子.一般HashMap的擴(kuò)容的臨界點(diǎn)是當(dāng)前HashMap的大小 > DEFAULT_LOAD_FACTOR * 
//DEFAULT_INITIAL_CAPACITY = 0.75F * 16
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//當(dāng)hash桶中的某個(gè)bucket上的結(jié)點(diǎn)數(shù)大于該值的時(shí)候,會(huì)由鏈表轉(zhuǎn)換為紅黑樹(shù)
static final int TREEIFY_THRESHOLD = 8;
//當(dāng)hash桶中的某個(gè)bucket上的結(jié)點(diǎn)數(shù)小于該值的時(shí)候,紅黑樹(shù)轉(zhuǎn)變?yōu)殒湵?
static final int UNTREEIFY_THRESHOLD = 6;
//桶中結(jié)構(gòu)轉(zhuǎn)化為紅黑樹(shù)對(duì)應(yīng)的table的最小大小
static final int MIN_TREEIFY_CAPACITY = 64;
//hash算法,計(jì)算傳入的key的hash值,下面會(huì)有例子說(shuō)明這個(gè)計(jì)算的過(guò)程
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
} 
//tableSizeFor(initialCapacity)返回大于initialCapacity的最小的二次冪數(shù)值。下面會(huì)有例子說(shuō)明
static final int tableSizeFor(int cap) {
    int n = cap - 1;
    n |= n >>> 1;
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
//hash桶
transient Node<K,V>[] table;
//保存緩存的entrySet
transient Set<Map.Entry<K,V>> entrySet;
//桶的實(shí)際元素個(gè)數(shù) != table.length
transient int size;
//擴(kuò)容或者更改了map的計(jì)數(shù)器。含義:表示這個(gè)HashMap結(jié)構(gòu)被修改的次數(shù),結(jié)構(gòu)修改是那些改變HashMap中的映射數(shù)量或者
//修改其內(nèi)部結(jié)構(gòu)(例如,重新散列rehash)的修改。 該字段用于在HashMap失敗快速(fast-fail)的Collection-views
//上創(chuàng)建迭代器。
transient int modCount;
//臨界值,當(dāng)實(shí)際大小(cap*loadFactor)大于該值的時(shí)候,會(huì)進(jìn)行擴(kuò)充
int threshold;
//加載因子
final float loadFactor;

2.1、hash方法說(shuō)明

//hash算法
static final int hash(Object key) {
    int h;
    //key == null : 返回hash=0
    //key != null 
    //(1)得到key的hashCode:h=key.hashCode()
    //(2)將h無(wú)符號(hào)右移16位
    //(3)異或運(yùn)算:h ^ h>>>16
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}   

​假設(shè)現(xiàn)在我們向一個(gè)map中添加元素,例如map.put("fsmly","test"),那么其中key為"fsmly"的hashCode的二進(jìn)制表示為0000_0000_0011_0110_0100_0100_1001_0010,按照上面的步驟來(lái)計(jì)算,那么我們調(diào)用hash算法得到的hash值為:‭

2.2、tableSizeFor方法說(shuō)明

​該方法的作用就是:返回大于initialCapacity的最小的二次冪數(shù)值。如下實(shí)例

//n=cap-1=5; 5的二進(jìn)制0101B。>>> 操作符表示無(wú)符號(hào)右移,高位取0
//n |= n>>>1: (1)n=0101 | 0101>>>1; (2)n=0101 | 0010; (3)n = 0111B 
//n |= n>>>2: (1)n=0111 | 0111>>>2; (2)n=0111 | 0011; (3)n = 0111B
//n |= n>>>4: (1)n=0111 | 0111>>>4; (2)n=0111 | 0000; (3)n = 0111B
//n |= n>>>8: (1)n=0111 | 0111>>>8; (2)n=0111 | 0000; (3)n = 0111B
//n |= n>>>16:(1)n=0111 | 0111>>>16;(2)n=0111 | 0000; (3)n = 0111B
static final int tableSizeFor(int cap) {
    int n = cap - 1;
    n |= n >>> 1;
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
    //n<0返回1
    //n>最大容量,返回最大容量
    //否則返回n+1(0111B+1B=1000B=8)
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

​再看下面這個(gè):

//至于這里為什么減1,當(dāng)傳入的cap為2的整數(shù)次冪的時(shí)候,減1即保證最后的計(jì)算結(jié)果還是cap,而不是大于cap的另一個(gè)2的
//整數(shù)次冪,例如我們傳入cap=16=10000B.按照上面那樣計(jì)算
//n=cap-1=15=1111B.按照上面的方法計(jì)算得到:
// n |= n>>>1: n=1111|0111=1111;后面還是相同的結(jié)果最后n=1111B=15.
//所以返回的時(shí)候?yàn)閞eturn 15+1;
int n = cap - 1; 

三、HashMap的構(gòu)造方法

​我們看看HashMap源碼中為我們提供的四個(gè)構(gòu)造方法。我們可以看到,平常我們最常用的無(wú)參構(gòu)造器內(nèi)部只是僅僅初始化了loadFactor,別的都沒(méi)有做,底層的數(shù)據(jù)結(jié)構(gòu)則是延遲到插入鍵值對(duì)時(shí)再進(jìn)行初始化,或者說(shuō)在resize中會(huì)做。后面說(shuō)到擴(kuò)容方法的實(shí)現(xiàn)的時(shí)候會(huì)講到。

//(1)參數(shù)為初始化容量和加載因子的構(gòu)造函數(shù)
public HashMap(int initialCapacity, float loadFactor) {
    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);
    this.loadFactor = loadFactor;
    this.threshold = tableSizeFor(initialCapacity); //閾值為大于initialCapacity的最小二次冪
}
//(2)只給定初始化容量,那么加載因子就是默認(rèn)的加載因子:0.75
public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
//(3)加載因子為默認(rèn)的加載因子,但是這個(gè)時(shí)候的初始化容量是沒(méi)有指定的,后面調(diào)用put或者get方法的時(shí)候才resize
public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
//(4)將傳遞的map中的值調(diào)用putMapEntries加入新的map集合中,其中加載因子是默認(rèn)的加載因子
public HashMap(Map<? extends K, ? extends V> m) {
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    putMapEntries(m, false);
}

四、HashMap元素在數(shù)組中的位置

​不管增加、刪除、查找鍵值對(duì),定位到哈希桶數(shù)組的索引都是很關(guān)鍵的第一步,所以我們看看源碼怎樣通過(guò)hash()方法以及其他代碼確定一個(gè)元素在hash桶中的位置的。

//計(jì)算map中key的hash值
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
//這一小段代碼就是定位元素在桶中的位置。具體做法就是:容量n-1 & hash. 
//其中n是一個(gè)2的整數(shù)冪,而(n - 1) & hash其實(shí)質(zhì)就是hash%n,但
//是取余運(yùn)算的效率不如位運(yùn)算與,并且(n - 1) & hash也能保證散列均勻,不會(huì)產(chǎn)生只有偶數(shù)位有值的現(xiàn)象
p = tab[i = (n - 1) & hash];

​下面我們通過(guò)一個(gè)例子計(jì)算一下上面這個(gè)定位的過(guò)程,假設(shè)現(xiàn)在桶大小n為16.

​我們可以看到,這里的hash方法并不是用原有對(duì)象的hashcode最為最終的hash值,而是做了一定位運(yùn)算,大概因?yàn)槿绻?n-1)的值太小的話(huà),(n - 1) & hash的值就完全依靠hash的低位值,比如n-1為0000 1111,那么最終的值就完全依賴(lài)于hash值的低4位了,這樣的話(huà)hash的高位就玩完全失去了作用,h ^ (h >>> 16),通過(guò)這種方式,讓高位數(shù)據(jù)與低位數(shù)據(jù)進(jìn)行異或,也是變相的加大了hash的隨機(jī)性,這樣就不單純的依賴(lài)對(duì)象的hashcode方法了。

五、HashMap的put方法分析

5.1、put方法源碼分析

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

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 == null 或者table的長(zhǎng)度為0,調(diào)用resize方法進(jìn)行擴(kuò)容
    //這里也說(shuō)明:table 被延遲到插入新數(shù)據(jù)時(shí)再進(jìn)行初始化
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    // 這里就是調(diào)用了Hash算法的地方,具體的計(jì)算可參考后面寫(xiě)到的例子
    //這里定位坐標(biāo)的做法在上面也已經(jīng)說(shuō)到過(guò)
    if ((p = tab[i = (n - 1) & hash]) == null)
        // 如果計(jì)算得到的桶下標(biāo)值中的Node為null,就新建一個(gè)Node加入該位置(這個(gè)新的結(jié)點(diǎn)是在
        //table數(shù)組中)。而該位置的hash值就是調(diào)用hash()方法計(jì)算得到的key的hash值
        tab[i] = newNode(hash, key, value, null);
    //這里表示put的元素用自己key的hash值計(jì)算得到的下表和桶中的第一個(gè)位置元素產(chǎn)生了沖突,具體就是
    //(1)key相同,value不同
    //(2)只是通過(guò)hash值計(jì)算得到的下標(biāo)相同,但是key和value都不同。這里處理的方法就是鏈表和紅黑樹(shù)
    else {
        Node<K,V> e; K k;
        //上面已經(jīng)計(jì)算得到了該hash對(duì)應(yīng)的下標(biāo)i,這里p=tab[i]。這里比較的有:
        //(1)tab[i].hash是否等于傳入的hash。這里的tab[i]就是桶中的第一個(gè)元素
        //(2)比較傳入的key和該位置的key是否相同
        //(3)如果都相同,說(shuō)明是同一個(gè)key,那么直接替換對(duì)應(yīng)的value值(在后面會(huì)進(jìn)行替換)
        if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
            //將桶中的第一個(gè)元素賦給e,用來(lái)記錄第一個(gè)位置的值
            e = p;
        //這里判斷為紅黑樹(shù)。hash值不相等,key不相等;為紅黑樹(shù)結(jié)點(diǎn)
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); //加入紅黑樹(shù)
        //判斷為鏈表結(jié)點(diǎn)
        else {
            for (int binCount = 0; ; ++binCount) {
                //如果達(dá)到鏈表的尾部
                if ((e = p.next) == null) {
                    //在尾部插入新的結(jié)點(diǎn)
                    p.next = newNode(hash, key, value, null);
                    //前面的binCount是記錄鏈表長(zhǎng)度的,如果該值大于8,就會(huì)轉(zhuǎn)變?yōu)榧t黑樹(shù)
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                //如果在遍歷鏈表的時(shí)候,判斷得出要插入的結(jié)點(diǎn)的key和鏈表中間的某個(gè)結(jié)點(diǎn)的key相
                //同,就跳出循環(huán),后面也會(huì)更新舊的value值
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                //e = p.next。遍歷鏈表所用
                p = e;
            }
        }
        //判斷插入的是否存在HashMap中,上面e被賦值,不為空,則說(shuō)明存在,更新舊的鍵值對(duì)
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value; //用傳入的參數(shù)value更新舊的value值
            afterNodeAccess(e);
            return oldValue; //返回舊的value值
        }
    }
    //modCount修改
    ++modCount;
    //容量超出就擴(kuò)容
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

5.2、put方法執(zhí)行過(guò)程總結(jié)

​可以看到主要邏輯在put方法中調(diào)用了putVal方法,傳遞的參數(shù)是調(diào)用了hash()方法計(jì)算key的hash值,主要邏輯在putVal中??梢越Y(jié)合注釋熟悉這個(gè)方法的執(zhí)行,我在這里大概總結(jié)一下這個(gè)方法的執(zhí)行:

1.首先 (tab = table) == null || (n = tab.length) == 0這一塊判斷hash桶是否為null,如果為null那么會(huì)調(diào)用resize方法擴(kuò)容。后面我們會(huì)說(shuō)到這個(gè)方法

2.定位元素在桶中的位置,具體就是通過(guò)key的hash值和hash桶的長(zhǎng)度計(jì)算得到下標(biāo)i,如果計(jì)算到的位置處沒(méi)有元素(null),那么就新建結(jié)點(diǎn)然后添加到該位置。

3.如果table[i]處不為null,已經(jīng)有元素了,那么就表明產(chǎn)生hash沖突,這里可能是三種情況

①判斷key是不是一樣,如果key一樣,那么就將新的值替換舊的值;

②如果不是因?yàn)閗ey一樣,那么需要判斷當(dāng)前該桶是不是已經(jīng)轉(zhuǎn)為了紅黑樹(shù),是的話(huà)就構(gòu)造一個(gè)TreeNode結(jié)點(diǎn)插入紅黑樹(shù);

③不是紅黑樹(shù),就使用鏈地址法處理沖突問(wèn)題。這里主要就是遍歷鏈表,如果在遍歷過(guò)程中也找到了key一樣的元素,那么久還是使用新值替換舊值。否則會(huì)遍歷到鏈表結(jié)尾處,到這里就直接新添加一個(gè)Node結(jié)點(diǎn)插入鏈表,插入之后還需要判斷是不是已將超過(guò)了轉(zhuǎn)換為紅黑樹(shù)的閾值8,如果超過(guò)就會(huì)轉(zhuǎn)為紅黑樹(shù)。

4.最后需要修改modCount的值。

5.判斷插入后的size大小是不是超過(guò)了threshhold,如果超過(guò)需要進(jìn)行擴(kuò)容。

上面很多地方都涉及到了擴(kuò)容,所以下面我們首先看看擴(kuò)容方法。

六、HashMap的resize方法分析

6.1、resize方法源碼

​擴(kuò)容(resize)就是重新計(jì)算容量,具體就是當(dāng)map內(nèi)部的size大于DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY ,就需要擴(kuò)大數(shù)組的長(zhǎng)度,以便能裝入更多的元素。resize方法實(shí)現(xiàn)中是使用一個(gè)新的數(shù)組代替已有的容量小的數(shù)組。

//該方法有2種使用情況:1.初始化哈希表(table==null) 2.當(dāng)前數(shù)組容量過(guò)小,需擴(kuò)容
final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table; //oldTab指向舊的table數(shù)組
    //oldTab不為null的話(huà),oldCap為原table的長(zhǎng)度
    //oldTab為null的話(huà),oldCap為0
    int oldCap = (oldTab == null) ? 0 : oldTab.length; 
    int oldThr = threshold; //閾值
    int newCap, newThr = 0;
    if (oldCap > 0) { 
        //這里表明oldCap!=0,oldCap=原table.length();
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE; //如果大于最大容量了,就賦值為整數(shù)最大的閥值
            return oldTab;
        }
        // 如果數(shù)組的長(zhǎng)度在擴(kuò)容后小于最大容量 并且oldCap大于默認(rèn)值16(這里的newCap也是在原來(lái)的
        //長(zhǎng)度上擴(kuò)展兩倍)
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold雙倍擴(kuò)展threshhold
    }
    else if (oldThr > 0) // initial capacity was placed in threshold
        //	這里的oldThr=tabSizeFor(initialCapacity),從上面的構(gòu)造方法看出,如果不是調(diào)用的
        //無(wú)參構(gòu)造,那么threshhold肯定都會(huì)是經(jīng)過(guò)tabSizeFor運(yùn)算得到的2的整數(shù)次冪的,所以可以將
        //其作為Node數(shù)組的長(zhǎng)度(個(gè)人理解)
        newCap = oldThr; 
    else { // zero initial threshold signifies using defaults(零初始閾值表示使用默認(rèn)值)
        //這里說(shuō)的是我們調(diào)用無(wú)參構(gòu)造函數(shù)的時(shí)候(table == null,threshhold = 0),新的容量等于默
        //認(rèn)的容量,并且threshhold也等于默認(rèn)加載因子*默認(rèn)初始化容量
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    // 計(jì)算新的resize上限
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor; //容量 * 加載因子
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    threshold = newThr;
    @SuppressWarnings({"rawtypes","unchecked"})
    //以新的容量作為長(zhǎng)度,創(chuàng)建一個(gè)新的Node數(shù)組存放結(jié)點(diǎn)元素
    //當(dāng)然,桶數(shù)組的初始化也是在這里完成的
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; 
    table = newTab;
    //原來(lái)的table不為null
    if (oldTab != null) {
        // 把每個(gè)bucket都移動(dòng)到新的buckets中
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            //原table中下標(biāo)j位置不為null
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null; //將原來(lái)的table[j]賦為null,及時(shí)GC?
                if (e.next == null) //如果該位置沒(méi)有鏈表,即只有數(shù)組中的那個(gè)元素
                    //通過(guò)新的容量計(jì)算在新的table數(shù)組中的下標(biāo):(n-1)&hash
                    newTab[e.hash & (newCap - 1)] = e; 
                else if (e instanceof TreeNode) 
                    //如果是紅黑樹(shù)結(jié)點(diǎn),重新映射時(shí),需要對(duì)紅黑樹(shù)進(jìn)行拆分
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                else { // 鏈表優(yōu)化重hash的代碼塊
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    do {//上面判斷不是紅黑樹(shù),那就是鏈表,這里就遍歷鏈表,進(jìn)行重新映射
                        next = e.next;
                        // 原位置
                        if ((e.hash & oldCap) == 0) {
                            //loTail處為null,那么直接加到該位置
                            if (loTail == null) 
                                loHead = e;
                            //loTail為鏈表尾結(jié)點(diǎn),添加到尾部
                            else
                                loTail.next = e;
                            //添加后,將loTail指向鏈表尾部,以便下次從尾部添加
                            loTail = e;
                        }
                        // 原位置+舊容量
                        else {
                            //hiTail處為null,就直接點(diǎn)添加到該位置
                            if (hiTail == null)
                                hiHead = e;
                            //hiTail為鏈表尾結(jié)點(diǎn),尾插法添加
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    // 將分組后的鏈表映射到新桶中
                    // 原索引放到bucket里
                    if (loTail != null) {
                        //舊鏈表遷移新鏈表,鏈表元素相對(duì)位置沒(méi)有變化; 
                        //實(shí)際是對(duì)對(duì)象的內(nèi)存地址進(jìn)行操作 
                        loTail.next = null;//鏈表尾元素設(shè)置為null
                        newTab[j] = loHead; //數(shù)組中位置為j的地方存放鏈表的head結(jié)點(diǎn)
                    }
                    // 原索引+oldCap放到bucket里
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

6.2、(e.hash & oldCap) == 0分析

​我這里添加上一點(diǎn),就是為什么使用 (e.hash & oldCap) == 0判斷是處于原位置還是放在更新的位置(原位置+舊容量),解釋如下:我們知道capacity是2的冪,所以oldCap為10...0的二進(jìn)制形式(比如16=10000B)。

(1)若判斷條件為真,意味著oldCap為1的那位對(duì)應(yīng)的hash位為0(1&0=0,其他位都是0,結(jié)果自然是0),對(duì)新索引的計(jì)算沒(méi)有影響,至于為啥沒(méi)影響下面就說(shuō)到了。先舉個(gè)例子計(jì)算一下數(shù)組中的下標(biāo)在擴(kuò)容前后的變化:

​從上面計(jì)算發(fā)現(xiàn),當(dāng)cap為1的那位對(duì)應(yīng)的hash為0的時(shí)候,resize前后的index是不變的。我們?cè)倏聪旅?,使用上面的hash值,對(duì)應(yīng)的就是 (e.hash & oldCap) == 0,恰好也是下標(biāo)不變的

​(2)若判斷條件為假,則 oldCap為1的那位對(duì)應(yīng)的hash位為1。比如新下標(biāo)=hash&( newCap-1 )= hash&( (16<<2) - 1)=10010,相當(dāng)于多了10000,即 oldCap .如同下面的例子

​從上面計(jì)算發(fā)現(xiàn),當(dāng)cap為1的那位對(duì)應(yīng)的hash為1的時(shí)候,resize前后的index是改變的。我們?cè)倏聪旅?,使用上面的hash值,對(duì)應(yīng)的就是 (e.hash & oldCap) != 0,恰好下標(biāo)就是原索引+原容量

6.3、部分代碼理解

​這一部分其實(shí)和put方法中,使用鏈地址法解決hash沖突的原理差不多,都是對(duì)鏈表的操作。

// 原位置
if ((e.hash & oldCap) == 0) {
    //loTail處為null,那么直接加到該位置
    if (loTail == null) 
        loHead = e;
    //loTail為鏈表尾結(jié)點(diǎn),添加到尾部
    else
        loTail.next = e;
    //添加后,將loTail指向鏈表尾部,以便下次從尾部添加
    loTail = e;
}
// 原位置+舊容量
else {
    //hiTail處為null,就直接點(diǎn)添加到該位置
    if (hiTail == null)
        hiHead = e;
    //hiTail為鏈表尾結(jié)點(diǎn),尾插法添加
    else
        hiTail.next = e;
    hiTail = e;
}

​我們直接通過(guò)一個(gè)簡(jiǎn)單的圖來(lái)理解吧

6.4、resize總結(jié)

​resize代碼稍微長(zhǎng)了點(diǎn),但是總結(jié)下來(lái)就是這幾點(diǎn)

判斷當(dāng)前oldTab長(zhǎng)度是否為空,如果為空,則進(jìn)行初始化桶數(shù)組,也就回答了無(wú)參構(gòu)造函數(shù)初始化為什么沒(méi)有對(duì)容量和閾值進(jìn)行賦值,如果不為空,則進(jìn)行位運(yùn)算,左移一位,2倍運(yùn)算擴(kuò)容。擴(kuò)容,創(chuàng)建一個(gè)新容量的數(shù)組,遍歷舊的數(shù)組:如果節(jié)點(diǎn)為空,直接賦值插入如果節(jié)點(diǎn)為紅黑樹(shù),則需要進(jìn)行進(jìn)行拆分操作(個(gè)人對(duì)紅黑樹(shù)還沒(méi)有理解,所以先不說(shuō)明)如果為鏈表,根據(jù)hash算法進(jìn)行重新計(jì)算下標(biāo),將鏈表進(jìn)行拆分分組(相信看到這里基本上也知道鏈表拆分的大致過(guò)程了)

七、HashMap的get方法分析

7.1、get方法源碼

​基本邏輯就是根據(jù)key算出hash值定位到哈希桶的索引,當(dāng)可以就是當(dāng)前索引的值則直接返回其對(duì)于的value,反之用key去遍歷equal該索引下的key,直到找到位置。

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;
    //計(jì)算存放在數(shù)組table中的位置.具體計(jì)算方法上面也已經(jīng)介紹了
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {
        //先查找是不是就是數(shù)組中的元素
        if (first.hash == hash && // always check first node
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        //該位置為紅黑樹(shù)根節(jié)點(diǎn)或者鏈表頭結(jié)點(diǎn)
        if ((e = first.next) != null) {
            //如果first為紅黑樹(shù)結(jié)點(diǎn),就在紅黑樹(shù)中遍歷查找
            if (first instanceof TreeNode)
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            //不是樹(shù)結(jié)點(diǎn),就在鏈表中遍歷查找
            do {
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null;
}

以上就是深入理解Java中的HashMap的詳細(xì)內(nèi)容,更多關(guān)于Java HashMap的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • Spring?Boot中@Validated注解不生效問(wèn)題匯總大全

    Spring?Boot中@Validated注解不生效問(wèn)題匯總大全

    這篇文章主要給大家介紹了關(guān)于Spring?Boot中@Validated注解不生效問(wèn)題匯總的相關(guān)資料,@Validated注解是Spring框架中的一個(gè)注解,用于在方法參數(shù)上添加參數(shù)校驗(yàn)規(guī)則,需要的朋友可以參考下
    2023-07-07
  • 分析JVM源碼之Thread.interrupt系統(tǒng)級(jí)別線(xiàn)程打斷

    分析JVM源碼之Thread.interrupt系統(tǒng)級(jí)別線(xiàn)程打斷

    在java編程中,我們經(jīng)常會(huì)調(diào)用Thread.sleep()方法使得線(xiàn)程停止運(yùn)行一段時(shí)間,而Thread類(lèi)中也提供了interrupt方法供我們?nèi)ブ鲃?dòng)打斷一個(gè)線(xiàn)程。那么線(xiàn)程掛起和打斷的本質(zhì)究竟是什么,本文就此問(wèn)題作一個(gè)探究
    2021-06-06
  • SpringDataJpa like查詢(xún)無(wú)效的解決

    SpringDataJpa like查詢(xún)無(wú)效的解決

    這篇文章主要介紹了SpringDataJpa like查詢(xún)無(wú)效的解決,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2021-12-12
  • Java集合之LinkedHashSet類(lèi)詳解

    Java集合之LinkedHashSet類(lèi)詳解

    這篇文章主要介紹了Java集合之LinkedHashSet類(lèi)詳解,LinkedHashSet 是 Java 中的一個(gè)集合類(lèi),它是 HashSet 的子類(lèi),并實(shí)現(xiàn)了 Set 接口,與 HashSet 不同的是,LinkedHashSet 保留了元素插入的順序,并且具有 HashSet 的快速查找特性,需要的朋友可以參考下
    2023-09-09
  • java工廠(chǎng)實(shí)例BeanFactoryPostProcessor和BeanPostProcessor區(qū)別分析

    java工廠(chǎng)實(shí)例BeanFactoryPostProcessor和BeanPostProcessor區(qū)別分析

    這篇文章主要為大家介紹了BeanFactoryPostProcessor和BeanPostProcessor區(qū)別示例分析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2023-07-07
  • java 中maven pom.xml文件教程詳解

    java 中maven pom.xml文件教程詳解

    這篇文章主要介紹了java 中maven pom.xml文件教程詳解,非常不錯(cuò),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2019-08-08
  • java單例模式學(xué)習(xí)示例

    java單例模式學(xué)習(xí)示例

    java中單例模式是一種常見(jiàn)的設(shè)計(jì)模式,單例模式分三種:懶漢式單例、餓漢式單例、登記式單例三種,下面提供了單例模式的示例
    2014-01-01
  • 挑戰(zhàn)4道Java試題

    挑戰(zhàn)4道Java試題

    這篇文章主要為大家分享了4道Java基礎(chǔ)題,幫助大家鞏固基礎(chǔ)知識(shí),夯實(shí)java基礎(chǔ)技能,感興趣的朋友快點(diǎn)挑戰(zhàn)
    2015-12-12
  • IDEA中java斷言assert語(yǔ)法及使用

    IDEA中java斷言assert語(yǔ)法及使用

    這篇文章主要介紹了IDEA中java斷言assert語(yǔ)法詳解,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2021-04-04
  • 使用Java生成JWT(JSON Web Token)的方法示例

    使用Java生成JWT(JSON Web Token)的方法示例

    在現(xiàn)代應(yīng)用程序中,身份驗(yàn)證和授權(quán)是至關(guān)重要的,JWT是一種簡(jiǎn)單而強(qiáng)大的身份驗(yàn)證和授權(quán)機(jī)制,可以在Web應(yīng)用程序中安全地傳輸用戶(hù)信息,本文主要介紹了使用Java生成JWT的方法示例,感興趣的可以了解一下
    2024-03-03

最新評(píng)論