看完这篇 HashMap ,和面试官扯皮就没问题了
來(lái)源 | Java?建設(shè)者
責(zé)編 | Carol
封圖 | CSDN 下載自視覺中國(guó)
(如果你沒有時(shí)間細(xì)摳本文,可以直接看 HashMap 概述,能讓你對(duì) HashMap 有個(gè)大致的了解)
HashMap 是 Map 接口的實(shí)現(xiàn),HashMap 允許空的 key-value 鍵值對(duì),HashMap 被認(rèn)為是 Hashtable 的增強(qiáng)版,HashMap 是一個(gè)非線程安全的容器,如果想構(gòu)造線程安全的 Map 考慮使用 ConcurrentHashMap。HashMap 是無(wú)序的,因?yàn)?HashMap 無(wú)法保證內(nèi)部存儲(chǔ)的鍵值對(duì)的有序性。
HashMap 的底層數(shù)據(jù)結(jié)構(gòu)是數(shù)組 + 鏈表的集合體,數(shù)組在 HashMap 中又被稱為桶(bucket)。遍歷 HashMap 需要的時(shí)間損耗為 HashMap 實(shí)例桶的數(shù)量 + (key - value 映射) 的數(shù)量。因此,如果遍歷元素很重要的話,不要把初始容量設(shè)置的太高或者負(fù)載因子設(shè)置的太低。
HashMap 實(shí)例有兩個(gè)很重要的因素,初始容量和負(fù)載因子,初始容量指的就是 hash 表桶的數(shù)量,負(fù)載因子是一種衡量哈希表填充程度的標(biāo)準(zhǔn),當(dāng)哈希表中存在足夠數(shù)量的 entry,以至于超過(guò)了負(fù)載因子和當(dāng)前容量,這個(gè)哈希表會(huì)進(jìn)行 rehash 操作,內(nèi)部的數(shù)據(jù)結(jié)構(gòu)重新 rebuilt。
注意 HashMap 不是線程安全的,如果多個(gè)線程同時(shí)影響了 HashMap ,并且至少一個(gè)線程修改了 HashMap 的結(jié)構(gòu),那么必須對(duì) HashMap 進(jìn)行同步操作。可以使用?Collections.synchronizedMap(new HashMap)?來(lái)創(chuàng)建一個(gè)線程安全的 Map。
HashMap 會(huì)導(dǎo)致除了迭代器本身的 remove 外,外部 remove 方法都可能會(huì)導(dǎo)致 fail-fast 機(jī)制,因此盡量要用迭代器自己的 remove 方法。如果在迭代器創(chuàng)建的過(guò)程中修改了 map 的結(jié)構(gòu),就會(huì)拋出?ConcurrentModificationException?異常。
下面就來(lái)聊一聊 HashMap 的細(xì)節(jié)問(wèn)題。我們還是從面試題入手來(lái)分析 HashMap 。
HashMap 和 HashTable 的區(qū)別
我們上面介紹了一下 HashMap ,現(xiàn)在來(lái)介紹一下 HashTable。
相同點(diǎn)
HashMap 和 HashTable 都是基于哈希表實(shí)現(xiàn)的,其內(nèi)部每個(gè)元素都是?key-value?鍵值對(duì),HashMap 和 HashTable 都實(shí)現(xiàn)了 Map、Cloneable、Serializable 接口。
不同點(diǎn)
父類不同:HashMap 繼承了?AbstractMap?類,而 HashTable 繼承了?Dictionary?類
空值不同:HashMap 允許空的 key 和 value 值,HashTable 不允許空的 key 和 value 值。HashMap 會(huì)把 Null key 當(dāng)做普通的 key 對(duì)待。不允許 null key 重復(fù)。
線程安全性:HashMap 不是線程安全的,如果多個(gè)外部操作同時(shí)修改 HashMap 的數(shù)據(jù)結(jié)構(gòu)比如 add 或者是 delete,必須進(jìn)行同步操作,僅僅對(duì) key 或者 value 的修改不是改變數(shù)據(jù)結(jié)構(gòu)的操作。可以選擇構(gòu)造線程安全的 Map 比如?Collections.synchronizedMap?或者是?ConcurrentHashMap。而 HashTable 本身就是線程安全的容器。
性能方面:雖然 HashMap 和 HashTable 都是基于單鏈表的,但是 HashMap 進(jìn)行 put 或者 get???? 操作,可以達(dá)到常數(shù)時(shí)間的性能;而 HashTable 的 put 和 get 操作都是加了?synchronized?鎖的,所以效率很差。
初始容量不同:HashTable 的初始長(zhǎng)度是11,之后每次擴(kuò)充容量變?yōu)橹暗?2n+1(n為上一次的長(zhǎng)度)
而 HashMap 的初始長(zhǎng)度為16,之后每次擴(kuò)充變?yōu)樵瓉?lái)的兩倍。創(chuàng)建時(shí),如果給定了容量初始值,那么HashTable 會(huì)直接使用你給定的大小,而 HashMap 會(huì)將其擴(kuò)充為2的冪次方大小。
HashMap 和 HashSet 的區(qū)別
也經(jīng)常會(huì)問(wèn)到 HashMap 和 HashSet 的區(qū)別
HashSet 繼承于 AbstractSet 接口,實(shí)現(xiàn)了 Set、Cloneable,、java.io.Serializable 接口。HashSet 不允許集合中出現(xiàn)重復(fù)的值。HashSet 底層其實(shí)就是 HashMap,所有對(duì) HashSet 的操作其實(shí)就是對(duì) HashMap 的操作。所以 HashSet 也不保證集合的順序。
HashMap 底層結(jié)構(gòu)
要了解一個(gè)類,先要了解這個(gè)類的結(jié)構(gòu),先來(lái)看一下 HashMap 的結(jié)構(gòu):
最主要的三個(gè)類(接口)就是?HashMap,AbstractMap和?Map?了,HashMap 我們上面已經(jīng)在概述中簡(jiǎn)單介紹了一下,下面來(lái)介紹一下 AbstractMap。
AbstractMap 類
這個(gè)抽象類是 Map 接口的骨干實(shí)現(xiàn),以求最大化的減少實(shí)現(xiàn)類的工作量。為了實(shí)現(xiàn)不可修改的 map,程序員僅需要繼承這個(gè)類并且提供 entrySet 方法的實(shí)現(xiàn)即可。它將會(huì)返回一組 map 映射的某一段。通常,返回的集合將在AbstractSet 之上實(shí)現(xiàn)。這個(gè)set不應(yīng)該支持 add 或者 remove 方法,并且它的迭代器也不支持 remove 方法。
為了實(shí)現(xiàn)可修改的 map,程序員必須額外重寫這個(gè)類的 put 方法(否則就會(huì)拋出UnsupportedOperationException),并且 entrySet.iterator() 返回的 iterator 必須實(shí)現(xiàn) remove() 方法。
Map 接口
Map 接口定義了 key-value 鍵值對(duì)的標(biāo)準(zhǔn)。一個(gè)對(duì)象支持 key-value 存儲(chǔ)。Map不能包含重復(fù)的 key,每個(gè)鍵最多映射一個(gè)值。這個(gè)接口代替了Dictionary 類,Dictionary是一個(gè)抽象類而不是接口。
Map 接口提供了三個(gè)集合的構(gòu)造器,它允許將 map 的內(nèi)容視為一組鍵,值集合或一組鍵值映射。map的順序定義為map映射集合上的迭代器返回其元素的順序。一些map實(shí)現(xiàn),像是TreeMap類,保證了map的有序性;其他的實(shí)現(xiàn),像是HashMap,則沒有保證。
重要內(nèi)部類和接口
Node 接口
Node節(jié)點(diǎn)是用來(lái)存儲(chǔ)HashMap的一個(gè)個(gè)實(shí)例,它實(shí)現(xiàn)了?Map.Entry接口,我們先來(lái)看一下 Map中的內(nèi)部接口 Entry 接口的定義
Map.Entry
//?一個(gè)map?的entry?鏈,這個(gè)Map.entrySet()方法返回一個(gè)集合的視圖,包含類中的元素, //?這個(gè)唯一的方式是從集合的視圖進(jìn)行迭代,獲取一個(gè)map的entry鏈。這些Map.Entry鏈只在 //?迭代期間有效。 interface?Entry<K,V>?{K?getKey();V?getValue();V?setValue(V?value);boolean?equals(Object?o);int?hashCode(); }Node 節(jié)點(diǎn)會(huì)存儲(chǔ)四個(gè)屬性,hash值,key,value,指向下一個(gè)Node節(jié)點(diǎn)的引用
?//?hash值 final?int?hash; //?鍵 final?K?key; //?值 V?value; //?指向下一個(gè)Node節(jié)點(diǎn)的Node類型 Node<K,V>?next;因?yàn)镸ap.Entry 是一條條entry 鏈連接在一起的,所以Node節(jié)點(diǎn)也是一條條entry鏈。構(gòu)造一個(gè)新的HashMap實(shí)例的時(shí)候,會(huì)把這四個(gè)屬性值分為傳入
Node(int?hash,?K?key,?V?value,?Node<K,V>?next)?{this.hash?=?hash;this.key?=?key;this.value?=?value;this.next?=?next; }實(shí)現(xiàn)了 Map.Entry 接口所以必須實(shí)現(xiàn)其中的方法,所以 Node 節(jié)點(diǎn)中也包括上面的五個(gè)方法
KeySet 內(nèi)部類
keySet 類繼承于 AbstractSet 抽象類,它是由 HashMap 中的?keyset()?方法來(lái)創(chuàng)建 KeySet 實(shí)例的,旨在對(duì)HashMap 中的key鍵進(jìn)行操作,看一個(gè)代碼示例
圖中把「1, 2, 3」這三個(gè)key 放在了HashMap中,然后使用 lambda 表達(dá)式循環(huán)遍歷 key 值,可以看到,map.keySet() 其實(shí)是返回了一個(gè) Set 接口,KeySet() 是在 Map 接口中進(jìn)行定義的,不過(guò)是被HashMap 進(jìn)行了實(shí)現(xiàn)操作,來(lái)看一下源碼就明白了
//?返回一個(gè)set視圖,這個(gè)視圖中包含了map中的key。 public?Set<K>?keySet()?{//?//?keySet?指向的是?AbstractMap?中的?keysetSet<K>?ks?=?keySet;if?(ks?==?null)?{//?如果?ks?為空,就創(chuàng)建一個(gè)?KeySet?對(duì)象//?并對(duì) ks 賦值。ks?=?new?KeySet();keySet?=?ks;}return?ks; }所以 KeySet 類中都是對(duì) Map中的 Key 進(jìn)行操作的:
Values 內(nèi)部類
Values 類的創(chuàng)建其實(shí)是和 KeySet 類很相似,不過(guò) KeySet 旨在對(duì) Map中的鍵進(jìn)行操作,Values 旨在對(duì)key-value?鍵值對(duì)中的 value 值進(jìn)行使用,看一下代碼示例:
循環(huán)遍歷 Map中的 values值,看一下 values() 方法最終創(chuàng)建的是什么:
所有的 values 其實(shí)都存儲(chǔ)在 AbstractMap 中,而 Values 類其實(shí)也是實(shí)現(xiàn)了 Map 中的 Values 接口,看一下對(duì) values 的操作都有哪些方法
其實(shí)是和 key 的操作差不多
EntrySet 內(nèi)部類
上面提到了HashMap中分別有對(duì) key、value 進(jìn)行操作的,其實(shí)還有對(duì)?key-value?鍵值對(duì)進(jìn)行操作的內(nèi)部類,它就是 EntrySet,來(lái)看一下EntrySet 的創(chuàng)建過(guò)程:
點(diǎn)進(jìn)去 entrySet() 會(huì)發(fā)現(xiàn)這個(gè)方法也是在 Map 接口中定義的,HashMap對(duì)它進(jìn)行了重寫
如果 es 為空創(chuàng)建一個(gè)新的 EntrySet 實(shí)例,EntrySet 主要包括了對(duì)key-value 鍵值對(duì)映射的方法,如下
HashMap 1.7 的底層結(jié)構(gòu)
JDK1.7 中,HashMap 采用位桶 + 鏈表的實(shí)現(xiàn),即使用鏈表來(lái)處理沖突,同一 hash 值的鏈表都存儲(chǔ)在一個(gè)數(shù)組中。但是當(dāng)位于一個(gè)桶中的元素較多,即 hash 值相等的元素較多時(shí),通過(guò) key 值依次查找的效率較低。它的數(shù)據(jù)結(jié)構(gòu)如下
HashMap 大致結(jié)構(gòu)
HashMap 底層數(shù)據(jù)結(jié)構(gòu)就是一個(gè) Entry 數(shù)組,Entry 是 HashMap 的基本組成單元,每個(gè) Entry 中包含一個(gè) key-value 鍵值對(duì)。
transient?Entry<K,V>[]?table?=?(Entry<K,V>[])?EMPTY_TABLE;而每個(gè) Entry 中包含?「hash, key ,value」?屬性,它是 HashMap 的一個(gè)內(nèi)部類
static?class?Entry<K,V>?implements?Map.Entry<K,V>?{final?K?key;V?value;Entry<K,V>?next;int?hash;Entry(int?h,?K?k,?V?v,?Entry<K,V>?n)?{value?=?v;next?=?n;key?=?k;hash?=?h;}... }所以,HashMap 的整體結(jié)構(gòu)就像下面這樣
HashMap 1.8 的底層結(jié)構(gòu)
與 JDK 1.7 相比,1.8 在底層結(jié)構(gòu)方面做了一些改變,當(dāng)每個(gè)桶中元素大于 8 的時(shí)候,會(huì)轉(zhuǎn)變?yōu)榧t黑樹,目的就是優(yōu)化查詢效率,JDK 1.8 重寫了?resize()?方法。
HashMap 重要屬性
「初始容量」
HashMap 的默認(rèn)初始容量是由?DEFAULT_INITIAL_CAPACITY?屬性管理的。
static?final?int?DEFAULT_INITIAL_CAPACITY?=?1?<<?4;HashMap 的默認(rèn)初始容量是 1 << 4 = 16, << 是一個(gè)左移操作,它相當(dāng)于是
「最大容量」
HashMap 的最大容量是
static?final?int?MAXIMUM_CAPACITY?=?1?<<?30;這里是不是有個(gè)疑問(wèn)?int 占用四個(gè)字節(jié),按說(shuō)最大容量應(yīng)該是左移 31 位,為什么 HashMap 最大容量是左移 30 位呢?因?yàn)樵跀?shù)值計(jì)算中,最高位也就是最左位的位?是代表著符號(hào)為,0 -> 正數(shù),1 -> 負(fù)數(shù),容量不可能是負(fù)數(shù),所以 HashMap 最高位只能移位到 2 ^ 30 次冪。
「默認(rèn)負(fù)載因子」
HashMap 的默認(rèn)負(fù)載因子是
static?final?float?DEFAULT_LOAD_FACTOR?=?0.75f;float 類型所以用?.f?為單位,負(fù)載因子是和擴(kuò)容機(jī)制有關(guān),這里大致提一下,后面會(huì)細(xì)說(shuō)。擴(kuò)容機(jī)制的原則是當(dāng) HashMap 中存儲(chǔ)的數(shù)量 > HashMap 容量 * 負(fù)載因子時(shí),就會(huì)把 HashMap 的容量擴(kuò)大為原來(lái)的二倍。
HashMap 的第一次擴(kuò)容就在 DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR = 12 時(shí)進(jìn)行。
「樹化閾值」
HashMap 的樹化閾值是
static?final?int?TREEIFY_THRESHOLD?=?8;在進(jìn)行添加元素時(shí),當(dāng)一個(gè)桶中存儲(chǔ)元素的數(shù)量 > 8 時(shí),會(huì)自動(dòng)轉(zhuǎn)換為紅黑樹(JDK1.8 特性)。
「鏈表閾值」
HashMap 的鏈表閾值是
static?final?int?UNTREEIFY_THRESHOLD?=?6;在進(jìn)行刪除元素時(shí),如果一個(gè)桶中存儲(chǔ)元素?cái)?shù)量 < 6 后,會(huì)自動(dòng)轉(zhuǎn)換為鏈表
「擴(kuò)容臨界值」
static?final?int?MIN_TREEIFY_CAPACITY?=?64;這個(gè)值表示的是當(dāng)桶數(shù)組容量小于該值時(shí),優(yōu)先進(jìn)行擴(kuò)容,而不是樹化
「節(jié)點(diǎn)數(shù)組」
HashMap 中的節(jié)點(diǎn)數(shù)組就是 Entry 數(shù)組,它代表的就是 HashMap 中?「數(shù)組 + 鏈表」?數(shù)據(jù)結(jié)構(gòu)中的數(shù)組。
transient?Node<K,V>[]?table;Node 數(shù)組在第一次使用的時(shí)候進(jìn)行初始化操作,在必要的時(shí)候進(jìn)行?resize,resize 后數(shù)組的長(zhǎng)度擴(kuò)容為原來(lái)的二倍。
「鍵值對(duì)數(shù)量」
在 HashMap 中,使用?size?來(lái)表示 HashMap 中鍵值對(duì)的數(shù)量。
「修改次數(shù)」
在 HashMap 中,使用?modCount?來(lái)表示修改次數(shù),主要用于做并發(fā)修改 HashMap 時(shí)的快速失敗 - fail-fast 機(jī)制。
「擴(kuò)容閾值」
在 HashMap 中,使用?threshold?表示擴(kuò)容的閾值,也就是 初始容量 * 負(fù)載因子的值。
threshold 涉及到一個(gè)擴(kuò)容的閾值問(wèn)題,這個(gè)問(wèn)題是由?tableSizeFor?源碼解決的。我們先看一下它的源碼再來(lái)解釋
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; }代碼中涉及一個(gè)運(yùn)算符?|=?,它表示的是按位或,啥意思呢?你一定知道?「a+=b 的意思是 a=a+b」,那么同理:a |= b 就是 a = a | b,也就是雙方都轉(zhuǎn)換為二進(jìn)制,來(lái)進(jìn)行與操作。如下圖所示
我們上面采用了一個(gè)比較大的數(shù)字進(jìn)行擴(kuò)容,由上圖可知 2^29 次方的數(shù)組經(jīng)過(guò)一系列的或操作后,會(huì)算出來(lái)結(jié)果是 2^30 次方。
所以擴(kuò)容后的數(shù)組長(zhǎng)度是原來(lái)的 2 倍。
「負(fù)載因子」
loadFactor?表示負(fù)載因子,它表示的是 HashMap 中的密集程度。
HashMap 構(gòu)造函數(shù)
在 HashMap 源碼中,有四種構(gòu)造函數(shù),分別來(lái)介紹一下
帶有初始容量 initialCapacity?和?負(fù)載因子 loadFactor?的構(gòu)造函數(shù)
初始容量不能為負(fù),所以當(dāng)傳遞初始容量 < 0 的時(shí)候,會(huì)直接拋出?IllegalArgumentException?異常。如果傳遞進(jìn)來(lái)的初始容量 > 最大容量時(shí),初始容量 = 最大容量。負(fù)載因子也不能小于 0 。然后進(jìn)行數(shù)組的擴(kuò)容,這個(gè)擴(kuò)容機(jī)制也非常重要,我們后面進(jìn)行探討
只帶有 initialCapacity 的構(gòu)造函數(shù)
最終也會(huì)調(diào)用到上面的構(gòu)造函數(shù),不過(guò)這個(gè)默認(rèn)的負(fù)載因子就是 HashMap 的默認(rèn)負(fù)載因子也就是?0.75f
無(wú)參數(shù)的構(gòu)造函數(shù)
默認(rèn)的負(fù)載因子也就是 0.75f
帶有 map 的構(gòu)造函數(shù)
帶有 Map 的構(gòu)造函數(shù),會(huì)直接把外部元素批量放入 HashMap 中。
講一講 HashMap put 的全過(guò)程
我記得剛畢業(yè)一年去北京面試,一家公司問(wèn)我 HashMap put 過(guò)程的時(shí)候,我支支吾吾答不上來(lái),后面痛下決心好好整。以 JDK 1.8 為基準(zhǔn)進(jìn)行分析,后面也是。先貼出整段代碼,后面會(huì)逐行進(jìn)行分析。
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?分配內(nèi)存,就resize一次if?((tab?=?table)?==?null?||?(n?=?tab.length)?==?0)n?=?(tab?=?resize()).length;//?指定hash值節(jié)點(diǎn)為空則直接插入,這個(gè)(n?-?1)?&?hash才是表中真正的哈希if?((p?=?tab[i?=?(n?-?1)?&?hash])?==?null)tab[i]?=?newNode(hash,?key,?value,?null);//?如果不為空else?{Node<K,V>?e;?K?k;//?計(jì)算表中的這個(gè)真正的哈希值與要插入的key.hash相比if?(p.hash?==?hash?&&((k?=?p.key)?==?key?||?(key?!=?null?&&?key.equals(k))))e?=?p;//?若不同的話,并且當(dāng)前節(jié)點(diǎn)已經(jīng)在?TreeNode?上了else?if?(p?instanceof?TreeNode)//?采用紅黑樹存儲(chǔ)方式e?=?((TreeNode<K,V>)p).putTreeVal(this,?tab,?hash,?key,?value);//?key.hash?不同并且也不再?TreeNode?上,在鏈表上找到?p.next==nullelse?{for?(int?binCount?=?0;?;?++binCount)?{if?((e?=?p.next)?==?null)?{//?在表尾插入p.next?=?newNode(hash,?key,?value,?null);//?新增節(jié)點(diǎn)后如果節(jié)點(diǎn)個(gè)數(shù)到達(dá)閾值,則進(jìn)入?treeifyBin()?進(jìn)行再次判斷if?(binCount?>=?TREEIFY_THRESHOLD?-?1)?//?-1?for?1sttreeifyBin(tab,?hash);break;}//?如果找到了同?hash、key?的節(jié)點(diǎn),那么直接退出循環(huán)if?(e.hash?==?hash?&&((k?=?e.key)?==?key?||?(key?!=?null?&&?key.equals(k))))break;//?更新?p?指向下一節(jié)點(diǎn)p?=?e;}}//?map中含有舊值,返回舊值if?(e?!=?null)?{?//?existing?mapping?for?keyV?oldValue?=?e.value;if?(!onlyIfAbsent?||?oldValue?==?null)e.value?=?value;afterNodeAccess(e);return?oldValue;}}//?map調(diào)整次數(shù)?+?1++modCount;//?鍵值對(duì)的數(shù)量達(dá)到閾值,需要擴(kuò)容if?(++size?>?threshold)resize();afterNodeInsertion(evict);return?null; }首先看一下?putVal?方法,這個(gè)方法是 final 的,如果你自已定義 HashMap 繼承的話,是不允許你自己重寫 put 方法的,然后這個(gè)方法涉及五個(gè)參數(shù)
hash -> put 放在桶中的位置,在 put 之前,會(huì)進(jìn)行 hash 函數(shù)的計(jì)算。
key -> 參數(shù)的 key 值
value -> 參數(shù)的 value 值
onlyIfAbsent -> 是否改變已經(jīng)存在的值,也就是是否進(jìn)行 value 值的替換標(biāo)志
evict -> 是否是剛創(chuàng)建 HashMap 的標(biāo)志
在調(diào)用到 putVal 方法時(shí),首先會(huì)進(jìn)行 hash 函數(shù)計(jì)算應(yīng)該插入的位置
public?V?put(K?key,?V?value)?{return?putVal(hash(key),?key,?value,?false,?true); }哈希函數(shù)的源碼如下
static?final?int?hash(Object?key)?{int?h;return?(key?==?null)???0?:?(h?=?key.hashCode())?^?(h?>>>?16); }首先先來(lái)理解一下 hash 函數(shù)的計(jì)算規(guī)則
Hash 函數(shù)
hash 函數(shù)會(huì)根據(jù)你傳遞的 key 值進(jìn)行計(jì)算,首先計(jì)算 key 的?hashCode?值,然后再對(duì) hashcode 進(jìn)行無(wú)符號(hào)右移操作,最后再和 hashCode 進(jìn)行異或 ^?操作。
?
>>>: 無(wú)符號(hào)右移操作,它指的是?「無(wú)符號(hào)右移,也叫邏輯右移,即若該數(shù)為正,則高位補(bǔ)0,而若該數(shù)為負(fù)數(shù),則右移后高位同樣補(bǔ)0」?,也就是不管是正數(shù)還是負(fù)數(shù),右移都會(huì)在空缺位補(bǔ) 0 。
?
在得到 hash 值后,就會(huì)進(jìn)行 put 過(guò)程。
首先會(huì)判斷 HashMap 中的 Node 數(shù)組是否為 null,如果第一次創(chuàng)建 HashMap 并進(jìn)行第一次插入元素,首先會(huì)進(jìn)行數(shù)組的 resize,也就是重新分配,這里還涉及到一個(gè)?resize()?擴(kuò)容機(jī)制源碼分析,我們后面會(huì)介紹。擴(kuò)容完畢后,會(huì)計(jì)算出 HashMap 的存放位置,通過(guò)使用?「( n - 1 ) & hash」?進(jìn)行計(jì)算得出。
然后會(huì)把這個(gè)位置作為數(shù)組的下標(biāo)作為存放元素的位置。如果不為空,那么計(jì)算表中的這個(gè)真正的哈希值與要插入的 key.hash 相比。如果哈希值相同,key-value 不一樣,再判斷是否是樹的實(shí)例,如果是的話,那么就把它插入到樹上。如果不是,就執(zhí)行尾插法在 entry 鏈尾進(jìn)行插入。
會(huì)根據(jù)桶中元素的數(shù)量判斷是鏈表還是紅黑樹。然后判斷鍵值對(duì)數(shù)量是否大于閾值,大于的話則進(jìn)行擴(kuò)容。
擴(kuò)容機(jī)制
在 Java 中,數(shù)組的長(zhǎng)度是固定的,這意味著數(shù)組只能存儲(chǔ)固定量的數(shù)據(jù)。但在開發(fā)的過(guò)程中,很多時(shí)候我們無(wú)法知道該建多大的數(shù)組合適。好在 HashMap 是一種自動(dòng)擴(kuò)容的數(shù)據(jù)結(jié)構(gòu),在這種基于變長(zhǎng)的數(shù)據(jù)結(jié)構(gòu)中,擴(kuò)容機(jī)制是非常重要的。
在 HashMap 中,閾值大小為桶數(shù)組長(zhǎng)度與負(fù)載因子的乘積。當(dāng) HashMap 中的鍵值對(duì)數(shù)量超過(guò)閾值時(shí),進(jìn)行擴(kuò)容。HashMap 中的擴(kuò)容機(jī)制是由?resize()?方法來(lái)實(shí)現(xiàn)的,下面我們就來(lái)一次認(rèn)識(shí)下。(貼出中文注釋,便于復(fù)制)
final?Node<K,V>[]?resize()?{Node<K,V>[]?oldTab?=?table;//?存儲(chǔ)old?table?的大小int?oldCap?=?(oldTab?==?null)???0?:?oldTab.length;//?存儲(chǔ)擴(kuò)容閾值int?oldThr?=?threshold;int?newCap,?newThr?=?0;if?(oldCap?>?0)?{//?如果old?table數(shù)據(jù)已達(dá)最大,那么threshold也被設(shè)置成最大if?(oldCap?>=?MAXIMUM_CAPACITY)?{threshold?=?Integer.MAX_VALUE;return?oldTab;}//?左移擴(kuò)大二倍,else?if?((newCap?=?oldCap?<<?1)?<?MAXIMUM_CAPACITY?&&oldCap?>=?DEFAULT_INITIAL_CAPACITY)//?擴(kuò)容成原來(lái)二倍newThr?=?oldThr?<<?1;?//?double?threshold}//?如果oldThr???????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????!>?0else?if?(oldThr?>?0)?//?initial?capacity?was?placed?in?thresholdnewCap?=?oldThr;//?如果old?table?<=?0?并且?存儲(chǔ)的閾值?<=?0else?{???????????????//?zero?initial?threshold?signifies?using?defaultsnewCap?=?DEFAULT_INITIAL_CAPACITY;newThr?=?(int)(DEFAULT_LOAD_FACTOR?*?DEFAULT_INITIAL_CAPACITY);}//?如果擴(kuò)充閾值為0if?(newThr?==?0)?{//?擴(kuò)容閾值為?初始容量*負(fù)載因子float?ft?=?(float)newCap?*?loadFactor;newThr?=?(newCap?<?MAXIMUM_CAPACITY?&&?ft?<?(float)MAXIMUM_CAPACITY??(int)ft?:?Integer.MAX_VALUE);}//?重新給負(fù)載因子賦值threshold?=?newThr;//?獲取擴(kuò)容后的數(shù)組@SuppressWarnings({"rawtypes","unchecked"})Node<K,V>[]?newTab?=?(Node<K,V>[])new?Node[newCap];table?=?newTab;//?如果第一次進(jìn)行table?初始化不會(huì)走下面的代碼//?擴(kuò)容之后需要重新把節(jié)點(diǎn)放在新擴(kuò)容的數(shù)組中if?(oldTab?!=?null)?{for?(int?j?=?0;?j?<?oldCap;?++j)?{Node<K,V>?e;if?((e?=?oldTab[j])?!=?null)?{oldTab[j]?=?null;if?(e.next?==?null)newTab[e.hash?&?(newCap?-?1)]?=?e;else?if?(e?instanceof?TreeNode)//?重新映射時(shí),需要對(duì)紅黑樹進(jìn)行拆分((TreeNode<K,V>)e).split(this,?newTab,?j,?oldCap);else?{?//?preserve?orderNode<K,V>?loHead?=?null,?loTail?=?null;Node<K,V>?hiHead?=?null,?hiTail?=?null;Node<K,V>?next;//?遍歷鏈表,并將鏈表節(jié)點(diǎn)按原順序進(jìn)行分組do?{next?=?e.next;if?((e.hash?&?oldCap)?==?0)?{if?(loTail?==?null)loHead?=?e;elseloTail.next?=?e;loTail?=?e;}else?{if?(hiTail?==?null)hiHead?=?e;elsehiTail.next?=?e;hiTail?=?e;}}?while?((e?=?next)?!=?null);//?將分組后的鏈表映射到新桶中if?(loTail?!=?null)?{loTail.next?=?null;newTab[j]?=?loHead;}if?(hiTail?!=?null)?{hiTail.next?=?null;newTab[j?+?oldCap]?=?hiHead;}}}}}return?newTab; }擴(kuò)容機(jī)制源碼比較長(zhǎng),我們耐心點(diǎn)進(jìn)行拆分
我們以 if...else if...else 邏輯進(jìn)行拆分,上面代碼主要做了這幾個(gè)事情
判斷 HashMap 中的數(shù)組的長(zhǎng)度,也就是?(Node<K,V>[])oldTab.length()?,再判斷數(shù)組的長(zhǎng)度是否比最大的的長(zhǎng)度也就是 2^30 次冪要大,大的話直接取最大長(zhǎng)度,否則利用位運(yùn)算?<<擴(kuò)容為原來(lái)的兩倍
如果數(shù)組長(zhǎng)度不大于0 ,再判斷擴(kuò)容閾值?threshold?是否大于 0 ,也就是看有無(wú)外部指定的擴(kuò)容閾值,若有則使用,這里需要說(shuō)明一下 threshold 何時(shí)是?oldThr > 0,因?yàn)?oldThr = threshold ,這里其實(shí)比較的就是 threshold,因?yàn)?HashMap 中的每個(gè)構(gòu)造方法都會(huì)調(diào)用?HashMap(initCapacity,loadFactor)?這個(gè)構(gòu)造方法,所以如果沒有外部指定 initialCapacity,初始容量使用的就是 16,然后根據(jù)?this.threshold = tableSizeFor(initialCapacity);?求得 threshold 的值。
否則,直接使用默認(rèn)的初始容量和擴(kuò)容閾值,走 else 的邏輯是在 table 剛剛初始化的時(shí)候。
然后會(huì)判斷 newThr 是否為 0 ,筆者在剛開始研究時(shí)發(fā)現(xiàn)?newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);?一直以為這是常量做乘法,怎么會(huì)為 0 ,其實(shí)不是這部分的問(wèn)題,在于上面邏輯判斷中的擴(kuò)容操作,可能會(huì)導(dǎo)致位溢出。
導(dǎo)致位溢出的示例:oldCap = 2^28 次冪,threshold > 2 的三次方整數(shù)次冪。在進(jìn)入到?float ft = (float)newCap * loadFactor;?這個(gè)方法是 2^28 * 2^(3+n) 會(huì)直接 > 2^31 次冪,導(dǎo)致全部歸零。
「在擴(kuò)容后需要把節(jié)點(diǎn)放在新擴(kuò)容的數(shù)組中,這里也涉及到三個(gè)步驟」
循環(huán)桶中的每個(gè) Node 節(jié)點(diǎn),判斷 Node[i] 是否為空,為空直接返回,不為空則遍歷桶數(shù)組,并將鍵值對(duì)映射到新的桶數(shù)組中。
如果不為空,再判斷是否是樹形結(jié)構(gòu),如果是樹形結(jié)構(gòu)則按照樹形結(jié)構(gòu)進(jìn)行拆分,拆分方法在?split?方法中。
如果不是樹形結(jié)構(gòu),則遍歷鏈表,并將鏈表節(jié)點(diǎn)按原順序進(jìn)行分組。
講一講 get 方法全過(guò)程
我們上面講了 HashMap 中的 put 方法全過(guò)程,下面我們來(lái)看一下?get?方法的過(guò)程,
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;//?找到真實(shí)的元素位置if?((tab?=?table)?!=?null?&&?(n?=?tab.length)?>?0?&&(first?=?tab[(n?-?1)?&?hash])?!=?null)?{//?總是會(huì)check?一下第一個(gè)元素if?(first.hash?==?hash?&&?//?always?check?first?node((k?=?first.key)?==?key?||?(key?!=?null?&&?key.equals(k))))return?first;//?如果不是第一個(gè)元素,并且下一個(gè)元素不是空的if?((e?=?first.next)?!=?null)?{//?判斷是否屬于?TreeNode,如果是?TreeNode?實(shí)例,直接從?TreeNode.getTreeNode?取if?(first?instanceof?TreeNode)return?((TreeNode<K,V>)first).getTreeNode(hash,?key);//?如果還不是?TreeNode?實(shí)例,就直接循環(huán)數(shù)組元素,直到找到指定元素位置do?{if?(e.hash?==?hash?&&((k?=?e.key)?==?key?||?(key?!=?null?&&?key.equals(k))))return?e;}?while?((e?=?e.next)?!=?null);}}return?null; }來(lái)簡(jiǎn)單介紹下吧,首先會(huì)檢查 table 中的元素是否為空,然后根據(jù) hash 算出指定 key 的位置。然后檢查鏈表的第一個(gè)元素是否為空,如果不為空,是否匹配,如果匹配,直接返回這條記錄;如果匹配,再判斷下一個(gè)元素的值是否為 null,為空直接返回,如果不為空,再判斷是否是?TreeNode?實(shí)例,如果是 TreeNode 實(shí)例,則直接使用?TreeNode.getTreeNode?取出元素,否則執(zhí)行循環(huán),直到下一個(gè)元素為 null 位置。
getNode?方法有一個(gè)比較重要的過(guò)程就是?「(n - 1) & hash」,這段代碼是確定需要查找的桶的位置的,那么,為什么要 (n - 1) & hash 呢?
n 就是 HashMap 中桶的數(shù)量,這句話的意思也就是說(shuō) (n - 1) & hash 就是 (桶的容量 - 1) & hash
//?為什么?HashMap?的檢索位置是?(table.size?-?1)?&?hash public?static?void?main(String[]?args)?{Map<String,Object>?map?=?new?HashMap<>();//?debug?得知?1?的?hash?值算出來(lái)是?49map.put("1","cxuan");//?debug?得知?1?的?hash?值算出來(lái)是?50map.put("2","cxuan");//?debug?得知?1?的?hash?值算出來(lái)是?51map.put("3","cxuan");}那么每次算完之后的 (n - 1) & hash ,依次為
也就是?「tab[(n - 1) & hash]」?算出的具體位置。
HashMap 的遍歷方式
HashMap 的遍歷,也是一個(gè)使用頻次特別高的操作
HashMap 遍歷的基類是?HashIterator,它是一個(gè) Hash 迭代器,它是一個(gè) HashMap 內(nèi)部的抽象類,它的構(gòu)造比較簡(jiǎn)單,只有三種方法,「hasNext 、 remove 和 nextNode」?方法,其中 nextNode 方法是由三種迭代器實(shí)現(xiàn)的
這三種迭代器就就是
KeyIterator?,對(duì) key 進(jìn)行遍歷
ValueIterator,對(duì) value 進(jìn)行遍歷
EntryIterator, 對(duì) Entry 鏈進(jìn)行遍歷
雖然說(shuō)看著迭代器比較多,但其實(shí)他們的遍歷順序都是一樣的,構(gòu)造也非常簡(jiǎn)單,都是使用?HashIterator?中的?nextNode?方法進(jìn)行遍歷
final?class?KeyIterator?extends?HashIteratorimplements?Iterator<K>?{public?final?K?next()?{?return?nextNode().key;?}}final?class?ValueIterator?extends?HashIteratorimplements?Iterator<V>?{public?final?V?next()?{?return?nextNode().value;?} }final?class?EntryIterator?extends?HashIteratorimplements?Iterator<Map.Entry<K,V>>?{public?final?Map.Entry<K,V>?next()?{?return?nextNode();?} }HashIterator 中的遍歷方式
abstract?class?HashIterator?{Node<K,V>?next;????????//?下一個(gè)?entry?節(jié)點(diǎn)Node<K,V>?current;?????//?當(dāng)前?entry?節(jié)點(diǎn)int?expectedModCount;??//?fail-fast?的判斷標(biāo)識(shí)int?index;?????????????//?當(dāng)前槽HashIterator()?{expectedModCount?=?modCount;Node<K,V>[]?t?=?table;current?=?next?=?null;index?=?0;if?(t?!=?null?&&?size?>?0)?{?//?advance?to?first?entrydo?{}?while?(index?<?t.length?&&?(next?=?t[index++])?==?null);}}public?final?boolean?hasNext()?{return?next?!=?null;}final?Node<K,V>?nextNode()?{Node<K,V>[]?t;Node<K,V>?e?=?next;if?(modCount?!=?expectedModCount)throw?new?ConcurrentModificationException();if?(e?==?null)throw?new?NoSuchElementException();if?((next?=?(current?=?e).next)?==?null?&&?(t?=?table)?!=?null)?{do?{}?while?(index?<?t.length?&&?(next?=?t[index++])?==?null);}return?e;}public?final?void?remove()?{...} }next 和 current 分別表示下一個(gè) Node 節(jié)點(diǎn)和當(dāng)前的 Node 節(jié)點(diǎn),HashIterator 在初始化時(shí)會(huì)遍歷所有的節(jié)點(diǎn)。下面我們用圖來(lái)表示一下他們的遍歷順序
你會(huì)發(fā)現(xiàn)?nextNode()?方法的遍歷方式和 HashIterator 的遍歷方式一樣,只不過(guò)判斷條件不一樣,構(gòu)造 HashIterator 的時(shí)候判斷條件是有沒有鏈表,桶是否為 null,而遍歷 nextNode 的判斷條件變?yōu)橄乱粋€(gè) node 節(jié)點(diǎn)是不是 null ,并且桶是不是為 null。
HashMap 中的移除方法
HashMap 中的移除方法也比較簡(jiǎn)單了,源碼如下
public?V?remove(Object?key)?{Node<K,V>?e;return?(e?=?removeNode(hash(key),?key,?null,?false,?true))?==?null??null?:?e.value; }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;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;if?(p.hash?==?hash?&&((k?=?p.key)?==?key?||?(key?!=?null?&&?key.equals(k))))node?=?p;else?if?((e?=?p.next)?!=?null)?{if?(p?instanceof?TreeNode)node?=?((TreeNode<K,V>)p).getTreeNode(hash,?key);else?{do?{if?(e.hash?==?hash?&&((k?=?e.key)?==?key?||(key?!=?null?&&?key.equals(k))))?{node?=?e;break;}p?=?e;}?while?((e?=?e.next)?!=?null);}}if?(node?!=?null?&&?(!matchValue?||?(v?=?node.value)?==?value?||(value?!=?null?&&?value.equals(v))))?{if?(node?instanceof?TreeNode)((TreeNode<K,V>)node).removeTreeNode(this,?tab,?movable);else?if?(node?==?p)tab[index]?=?node.next;elsep.next?=?node.next;++modCount;--size;afterNodeRemoval(node);return?node;}}return?null; }remove 方法有很多,最終都會(huì)調(diào)用到 removeNode 方法,只不過(guò)傳遞的參數(shù)值不同,我們拿 remove(object) 來(lái)演示一下。
首先會(huì)通過(guò) hash 來(lái)找到對(duì)應(yīng)的 bucket,然后通過(guò)遍歷鏈表,找到鍵值相等的節(jié)點(diǎn),然后把對(duì)應(yīng)的節(jié)點(diǎn)進(jìn)行刪除。
關(guān)于 HashMap 的面試題
HashMap 的數(shù)據(jù)結(jié)構(gòu)
JDK1.7 中,HashMap 采用位桶 + 鏈表的實(shí)現(xiàn),即使用鏈表來(lái)處理沖突,同一 hash 值的鏈表都存儲(chǔ)在一個(gè)數(shù)組中。但是當(dāng)位于一個(gè)桶中的元素較多,即 hash 值相等的元素較多時(shí),通過(guò) key 值依次查找的效率較低。
所以,與 JDK 1.7 相比,JDK 1.8 在底層結(jié)構(gòu)方面做了一些改變,當(dāng)每個(gè)桶中元素大于 8 的時(shí)候,會(huì)轉(zhuǎn)變?yōu)榧t黑樹,目的就是優(yōu)化查詢效率。
HashMap 的 put 過(guò)程
大致過(guò)程如下,首先會(huì)使用 hash 方法計(jì)算對(duì)象的哈希碼,根據(jù)哈希碼來(lái)確定在 bucket 中存放的位置,如果 bucket 中沒有 Node 節(jié)點(diǎn)則直接進(jìn)行 put,如果對(duì)應(yīng) bucket 已經(jīng)有 Node 節(jié)點(diǎn),會(huì)對(duì)鏈表長(zhǎng)度進(jìn)行分析,判斷長(zhǎng)度是否大于 8,如果鏈表長(zhǎng)度小于 8 ,在 JDK1.7 前會(huì)使用頭插法,在 JDK1.8 之后更改為尾插法。如果鏈表長(zhǎng)度大于 8 會(huì)進(jìn)行樹化操作,把鏈表轉(zhuǎn)換為紅黑樹,在紅黑樹上進(jìn)行存儲(chǔ)。
HashMap 為啥線程不安全
HashMap 不是一個(gè)線程安全的容器,不安全性體現(xiàn)在多線程并發(fā)對(duì) HashMap 進(jìn)行 put 操作上。如果有兩個(gè)線程 A 和 B ,首先 A 希望插入一個(gè)鍵值對(duì)到 HashMap 中,在決定好桶的位置進(jìn)行 put 時(shí),此時(shí) A 的時(shí)間片正好用完了,輪到 B 運(yùn)行,B 運(yùn)行后執(zhí)行和 A 一樣的操作,只不過(guò) B 成功把鍵值對(duì)插入進(jìn)去了。如果 A 和 B 插入的位置(桶)是一樣的,那么線程 A 繼續(xù)執(zhí)行后就會(huì)覆蓋 B 的記錄,造成了數(shù)據(jù)不一致問(wèn)題。
還有一點(diǎn)在于 HashMap 在擴(kuò)容時(shí),因 resize 方法會(huì)形成環(huán),造成死循環(huán),導(dǎo)致 CPU 飆高。
HashMap 是如何處理哈希碰撞的
HashMap 底層是使用位桶 + 鏈表實(shí)現(xiàn)的,位桶決定元素的插入位置,位桶是由 hash 方法決定的,當(dāng)多個(gè)元素的 hash 計(jì)算得到相同的哈希值后,HashMap 會(huì)把多個(gè) Node 元素都放在對(duì)應(yīng)的位桶中,形成鏈表,這種處理哈希碰撞的方式被稱為鏈地址法。
其他處理 hash 碰撞的方式還有?「開放地址法、rehash 方法、建立一個(gè)公共溢出區(qū)」這幾種方法。
HashMap 是如何 get 元素的
首先會(huì)檢查 table 中的元素是否為空,然后根據(jù) hash 算出指定 key 的位置。然后檢查鏈表的第一個(gè)元素是否為空,如果不為空,是否匹配,如果匹配,直接返回這條記錄;如果匹配,再判斷下一個(gè)元素的值是否為 null,為空直接返回,如果不為空,再判斷是否是?TreeNode?實(shí)例,如果是 TreeNode 實(shí)例,則直接使用?TreeNode.getTreeNode?取出元素,否則執(zhí)行循環(huán),直到下一個(gè)元素為 null 位置。
HashMap 和 HashTable 有什么區(qū)別
見上
HashMap 和 HashSet 的區(qū)別
見上
HashMap 是如何擴(kuò)容的
HashMap 中有兩個(gè)非常重要的變量,一個(gè)是?loadFactor?,一個(gè)是?threshold?,loadFactor 表示的就是負(fù)載因子,threshold 表示的是下一次要擴(kuò)容的閾值,當(dāng) threshold = loadFactor * 數(shù)組長(zhǎng)度時(shí),數(shù)組長(zhǎng)度擴(kuò)大位原來(lái)的兩倍,來(lái)重新調(diào)整 map 的大小,并將原來(lái)的對(duì)象放入新的 bucket 數(shù)組中。
HashMap 的長(zhǎng)度為什么是 2 的冪次方
這道題我想了幾天,之前和群里小伙伴們探討每日一題的時(shí)候,問(wèn)他們?yōu)槭裁?length%hash == (n - 1) & hash,它們說(shuō)相等的前提是 length 的長(zhǎng)度 2 的冪次方,然后我回了一句難道 length 還能不是 2 的冪次方嗎?其實(shí)是我沒有搞懂因果關(guān)系,因?yàn)?HashMap 的長(zhǎng)度是 2 的冪次方,所以使用余數(shù)來(lái)判斷在桶中的下標(biāo)。如果 length 的長(zhǎng)度不是 2 的冪次方,小伙伴們可以舉個(gè)例子來(lái)試試。
例如長(zhǎng)度為 9 時(shí)候,3 & (9-1) = 0,2 & (9-1) = 0 ,都在 0 上,碰撞了;
這樣會(huì)增大 HashMap 碰撞的幾率。
HashMap 線程安全的實(shí)現(xiàn)有哪些
因?yàn)?HashMap 不是一個(gè)線程安全的容器,所以并發(fā)場(chǎng)景下推薦使用?ConcurrentHashMap?,或者使用線程安全的 HashMap,使用?Collections?包下的線程安全的容器,比如說(shuō)
Collections.synchronizedMap(new?HashMap());還可以使用 HashTable ,它也是線程安全的容器,基于 key-value 存儲(chǔ),經(jīng)常用 HashMap 和 HashTable 做比較就是因?yàn)?HashTable 的數(shù)據(jù)結(jié)構(gòu)和 HashMap 相同。
上面效率最高的就是 ConcurrentHashMap。
文章并沒有敘述太多關(guān)于紅黑樹的構(gòu)造、包含添加、刪除、樹化等過(guò)程,一方面是自己能力還達(dá)不到,一方面是關(guān)于紅黑樹的描述太過(guò)于占據(jù)篇幅,紅黑樹又是很大的一部分內(nèi)容,所以會(huì)考慮放在后續(xù)的紅黑樹進(jìn)行講解。
推薦閱讀淺談分布式存儲(chǔ)中的網(wǎng)絡(luò)通信
138 張圖帶你 MySQL 入門!
如何在 Kubernetes 上配置 Jenkins?
突發(fā)!印度封禁抖音、微信、快手等 59 款中國(guó) App
厲害!國(guó)內(nèi)大學(xué)生計(jì)算機(jī)編程第一人,一人挑戰(zhàn)一個(gè)隊(duì),百度最年輕 T10,現(xiàn)創(chuàng)業(yè)自動(dòng)駕駛
Balancer因通縮代幣STA遭遇閃電貸攻擊,價(jià)值50萬(wàn)美元資產(chǎn)被黑
淺談分布式存儲(chǔ)中的網(wǎng)絡(luò)通信
真香,朕在看了!
總結(jié)
以上是生活随笔為你收集整理的看完这篇 HashMap ,和面试官扯皮就没问题了的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 数据中台 VS 传统大数据平台,这 8
- 下一篇: Kubernetes 稳定性保障手册(极