深入理解Java中的HashMap
一、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)題匯總大全
這篇文章主要給大家介紹了關(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)程打斷
在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-06SpringDataJpa like查詢(xún)無(wú)效的解決
這篇文章主要介紹了SpringDataJpa like查詢(xún)無(wú)效的解決,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-12-12java工廠(chǎng)實(shí)例BeanFactoryPostProcessor和BeanPostProcessor區(qū)別分析
這篇文章主要為大家介紹了BeanFactoryPostProcessor和BeanPostProcessor區(qū)別示例分析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-07-07使用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