1. HashMap的實(shí)現(xiàn)
1.1 Hash的實(shí)現(xiàn)

1.2. 底層實(shí)現(xiàn)
1.2.1. JDK1.8之前
拉鏈法
創(chuàng)建一個(gè)鏈表數(shù)組,數(shù)組中每一格就是一個(gè)鏈表。若遇到哈希沖突,則將沖突的值加到鏈表中即可。

1.2.2. JDK1.8之后
鏈表長度大于閾值(默認(rèn)為8)時(shí),將鏈表轉(zhuǎn)化為紅黑樹,以減少搜索時(shí)間。

2. Jdk 1.7和1.8 hashMap 區(qū)別
2.1 擴(kuò)容時(shí)方式不同
在移動(dòng)鏈表時(shí),1.7是頭插法,1.8尾插法,導(dǎo)致1.7鏈表元素順序會(huì)顛倒,1.8不會(huì)
確定元素新的索引時(shí),1.8只需要看多出來的一位index是0還是1,1.7需要重新計(jì)算
h&(length - 1)
2.1.1 頭插法和尾插法
頭插
新增在鏈表上元素的位置為鏈表頭部,也就是數(shù)組桶位上的那個(gè)位置
可能是考慮到熱點(diǎn)數(shù)據(jù)的原因,即最近插入的元素也很可能最近會(huì)被使用到
尾插
為了避免出現(xiàn)逆序?qū)е碌某森h(huán)的問題
2.1.2 頭插法導(dǎo)致的后果(1.7并發(fā)為何不安全)
在transfer()方法中,因?yàn)樾碌?code>Table順序和舊的不同,所以在多線程同時(shí)擴(kuò)容情況下,會(huì)導(dǎo)致第二個(gè)擴(kuò)容的線程next混亂,本來是A -> B,但t1線程已經(jīng)B -> A了,所以就成環(huán)了。
2.1.3 Java1.8如何解決成環(huán)的問題的
1.8扔掉了transfer()方法,用resize()擴(kuò)容:
使用do while循環(huán)一次將一個(gè)鏈表上的所有元素加到鏈表上,然后再放到新的Table上對應(yīng)的索引位置。
2.2 底層結(jié)構(gòu)不同
JDK1.7的時(shí)候使用的是數(shù)組+ 單鏈表的數(shù)據(jù)結(jié)構(gòu)。但是在JDK1.8及之后時(shí),使用的是數(shù)組+鏈表+紅黑樹的數(shù)據(jù)結(jié)構(gòu)(當(dāng)鏈表的深度達(dá)到8的時(shí)候,也就是默認(rèn)閾值,就會(huì)自動(dòng)擴(kuò)容把鏈表轉(zhuǎn)成紅黑樹的數(shù)據(jù)結(jié)構(gòu)來把時(shí)間復(fù)雜度從O(n)變成O(logN)提高了效率)
2.3 索引計(jì)算方式不同
1.8的索引 只用了一次移位,一次位運(yùn)算就確定了索引,計(jì)算過程優(yōu)化。
二者的hash擾動(dòng)函數(shù)也不同,1.7有4次移位和5次位運(yùn)算,1.8只有一次移位和一次位運(yùn)算
3. HashMap參數(shù)理解
3.1 常見參數(shù)
初始容量和加載因子會(huì)影響HashMap的性能:
- 初始容量設(shè)置過小會(huì)導(dǎo)致大量擴(kuò)容(最好預(yù)估一下)
- 加載因子設(shè)置不當(dāng),導(dǎo)致頻繁擴(kuò)容操作
3.1.1 capacity
常說的capacity指的是DEFAULT_INITIAL_CAPACITY(初始容量),值是1<<4,即16;
capacity()是個(gè)方法,返回?cái)?shù)組的長度。
final int capacity() {
return (table != null) ? table.length :
(threshold > 0) ? threshold :
DEFAULT_INITIAL_CAPACITY;
}
3.1.2 loadFactor(加載因子)
final float loadFactor;
在hashMap構(gòu)造函數(shù)中,賦值為DEFAULT_LOAD_FACTOR(0.75f)
加載因子可設(shè)為>1,即永不會(huì)擴(kuò)容,(犧牲性能節(jié)省內(nèi)存)
3.1.3 size
Map中現(xiàn)在有的鍵值對數(shù)量,每put一個(gè)entry,++size
The number of key-value mappings contained in this map.
3.1.4 threshold
數(shù)組擴(kuò)容閾值。
即:HashMap數(shù)組總?cè)萘?* 加載因子(16 * 0.75 = 12)。當(dāng)前size大于或等于該值時(shí)會(huì)執(zhí)行擴(kuò)容resize()。擴(kuò)容的容量為當(dāng)前 HashMap 總?cè)萘康膬杀?。比如,?dāng)前 HashMap 的總?cè)萘繛?16 ,那么擴(kuò)容之后為 32
3.2 關(guān)于樹
樹形化閾值TREEIFY_THRESHOLD。當(dāng)鏈表的節(jié)點(diǎn)個(gè)數(shù)大于等于這個(gè)值時(shí),會(huì)將鏈表轉(zhuǎn)化為紅黑樹。
解除樹形化閾值UNTREEIFY_THRESHOLD 。當(dāng)鏈表的節(jié)點(diǎn)個(gè)數(shù)小于等于這個(gè)值時(shí),會(huì)將紅黑樹轉(zhuǎn)換成普通的鏈表
MIN_TREEIFY_CAPACITY 樹形化閾值的第二條件。當(dāng)數(shù)組的長度小于這個(gè)值時(shí),就算樹形化閾達(dá)標(biāo),鏈表也不會(huì)轉(zhuǎn)化為紅黑樹,而是優(yōu)先擴(kuò)容數(shù)組resize()
4. hashCode
4.1 hashCode()
獲取哈希碼,object的hashCode()方法是個(gè)本地方法,是由C實(shí)現(xiàn)的。
理論上hashCode是一個(gè)int值,這個(gè)int值范圍在-2147483648和2147483648之間,如果直接拿這個(gè)hashCode作為HashMap中數(shù)組的下標(biāo)來訪問的話,正常情況下是不會(huì)出現(xiàn)hash碰撞的。
但是這樣的話會(huì)導(dǎo)致這個(gè)HashMap的數(shù)組長度比較長,長度大概為40億,內(nèi)存肯定是放不下的,所以這個(gè)時(shí)候需要把這個(gè)hashCode對數(shù)組長度取余,用得到的余數(shù)來訪問數(shù)組下標(biāo)。
4.2 計(jì)算索引的過程
- 調(diào)用hashCode()得到一串?dāng)?shù)字超長的數(shù)字h
- 將h右移16位,與h按位異或(^),得到hash(擾動(dòng)函數(shù))
- 將(n-1)與hash按位與(&)
此處n-1指 (1111)_2(即15),因?yàn)閿?shù)組默認(rèn)長度16(n) - 得到下標(biāo)
你知道hash的實(shí)現(xiàn)嗎?為什么要這樣實(shí)現(xiàn)?
在Java 1.8的實(shí)現(xiàn)中,是通過把計(jì)算得到的hashCode()的右移16位,異或低16位實(shí)現(xiàn)的:(h = k.hashCode()) ^ (h >>> 16),這里得到的
hash與(n-1)按位與之后就是索引。主要是從速度、功效、質(zhì)量來考慮的,
- 這么做保證高低bit都參與到hash的計(jì)算中,保留了一部分高位的信息(加大了低位隨機(jī)性,減少了沖突)(如果不移位,高位信息就用不上)
- 同時(shí)不會(huì)有太大的開銷。
高低位異或,避免高位不同,低位相同的兩個(gè)hashCode值 產(chǎn)生碰撞。
Java1.7中 hash的實(shí)現(xiàn)
static int hash(int h) {
// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
/**
* Returns index for hash code h.
*/
static int indexFor(int h, int length) {
return h & (length-1);
//這里為什么用&位運(yùn)算??(下文)
}
一個(gè)數(shù)對2^n取模 == 一個(gè)數(shù)和(2^n – 1)做按位與運(yùn)算 。
X % 2^n == X & (2^n – 1)
//2^n表示2的n次方
假設(shè)n為3,則2^3 = 8,表示成2進(jìn)制就是1000。2^3 -1 = 7 ,即0111。
假設(shè)x=18,也就是 0001 0010,一起看下結(jié)果:
對 2的n次方 取模: 18 % 8 = 2
對 2的n次方-1 按位與: 0001 0010 & 0111 = 0000 0010 = 2
4.3 HashCode在equals方法中的作用
為什么重寫 equals 時(shí)必須重寫 hashCode 方法?
hashCode()的默認(rèn)行為是對堆上的對象產(chǎn)生獨(dú)特值。如果沒有重寫hashCode(),則該 class 的兩個(gè)對象無論如何都不會(huì)相等(即使這兩個(gè)對象指向相同的數(shù)據(jù))hashCode 方法的常規(guī)協(xié)定聲明 相等對象必須具有相等的哈希碼 。
equals()既然已能對比的功能了,為什么還要hashCode()呢?因?yàn)橹貙懙膃quals()里一般比較的比較全面比較復(fù)雜,這樣效率就比較低,而利用hashCode()進(jìn)行對比,則只要生成一個(gè)hash值進(jìn)行比較就可以了,效率很高。
hashcode只是用來縮小查找成本
hashCode()既然效率這么高為什么還要equals()呢?因?yàn)閔ashCode()并不是完全可靠,有時(shí)候不同的對象他們生成的hashcode也會(huì)一樣(生成hash值得公式可能存在的問題),所以hashCode()只能說是大部分時(shí)候可靠,并不是絕對可靠,
5. RESIZE(擴(kuò)容)
5.1 為什么擴(kuò)容
hashMap是懶加載的,第一次用之前,Table都是空的,第一次用需要擴(kuò)容
-
當(dāng)容量(
size屬性 * )達(dá)到閾值threshold減少hash沖突-
size屬性就是指所有的<k,v>數(shù)量,不管在不在同一個(gè)鏈表內(nèi)都算;下文是putValue()源碼,這個(gè)++size是在所有if else之外的,所以不管新節(jié)點(diǎn)放到哪size都會(huì)增加。if (++size > threshold) resize();
-
5.2 擴(kuò)容到多少?
自動(dòng)擴(kuò)容的容量為當(dāng)前 HashMap 總?cè)萘康膬杀叮?/p>
-
如果是指定了容量,
tableSizeFor函數(shù)會(huì)根據(jù)傳入的參數(shù),尋找距離最近的2的N次方;傳75,容量就是128.http://www.itdecent.cn/p/f86191afd918 (tableSizeFor函數(shù)源碼解析)
5.3 為什么擴(kuò)容是2倍?
容量是2的次冪,結(jié)合計(jì)算索引的位運(yùn)算,可以比較大程度分散元素,散列效果更好
-
計(jì)算索引會(huì)更高效(尤其是擴(kuò)容時(shí),用容量n和hash按位與很方便高效,(實(shí)際上是查看最高位是0/1))
計(jì)算索引本質(zhì)是取模運(yùn)算,當(dāng)容量是2的次冪時(shí),取??梢赞D(zhuǎn)化為快速的位運(yùn)算。
HashMap為了存取高效,要盡量較少碰撞,就是要盡量把數(shù)據(jù)分配均勻,每個(gè)鏈表長度大致相同,這個(gè)實(shí)現(xiàn)就在把數(shù)據(jù)存到哪個(gè)鏈表中的算法;
這個(gè)算法實(shí)際就是取模,hash%length。 但是,大家都知道這種運(yùn)算不如位移運(yùn)算快
Jdk1.8中,用的是
hash&(length-1),如果容量是2的n次方,在計(jì)算索引時(shí),(length - 1)的二進(jìn)制形式上每一位都是1,這樣與hash進(jìn)行按位與會(huì)更大程度的分散元素,例如長度為8時(shí)候,3&(8-1)=3 2&(8-1)=2 ,不同位置上,不碰撞。 而長度為5的時(shí)候,3&(5-1)=0 2&(5-1)=0,都在0上,出現(xiàn)碰撞了
5.3 什么時(shí)候擴(kuò)容
A:兩個(gè)時(shí)候
- HashMap實(shí)行了懶加載, 新建HashMap時(shí)不會(huì)對table進(jìn)行賦值, 而是到第一次插入時(shí), 進(jìn)行resize時(shí)構(gòu)建table;當(dāng)新建時(shí), 如果沒有指定HashMap.table的初始長度, 就用默認(rèn)值16, 否則就是指定的值; 然后不管是第一次構(gòu)建還是后續(xù)擴(kuò)容, threshold = capacity * loadFactor(比如往HashMap里面放了一對值,threshold就會(huì)更新)
- 當(dāng)HashMap.size 大于 threshold時(shí), 會(huì)進(jìn)行resize;threshold的值:
5.4 loadFactor為什么是0.75
Node[] table,即哈希桶數(shù)組,哈希桶數(shù)組table的長度length大小必須為2的n次方
0.75 * 2^n 得到的都是整數(shù)。
5.5 HashCode的計(jì)算在擴(kuò)容上的好處
把bucket擴(kuò)充為2倍,之后重新計(jì)算index,把節(jié)點(diǎn)再放到新的bucket中
hashCode是很長的一串?dāng)?shù)字,<font color = orange>(換成二進(jìn)制,此元素的位置就是后四位組成的 (數(shù)組的長度為16,即4位))</font>
擴(kuò)容 = 把作為索引的數(shù)字范圍往前一個(gè)
eg.
<font color = gray>1111 1111 1111 1111 0000 1111 0001</font> 1111 (原索引是后面四個(gè),索引是15)
擴(kuò)容后:
<font color = gray>1111 1111 1111 1111 0000 1111 000</font>1 1111 (新的索引多了一位)(多出來這個(gè),或1或0 隨機(jī),完全看hash)
因此,我們在擴(kuò)充HashMap的時(shí)候,不需要重新計(jì)算hash,只需要看看原來的hash值新增的那個(gè)bit是1還是0就好了,是0的話索引沒變,是1的話索引變成“原索引+oldCap”。
if ((e.hash & oldCap) == 0) {// oldCap == 1000 按位與就是看第一位是不是1
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
這個(gè)設(shè)計(jì)確實(shí)非常的巧妙,既省去了重新計(jì)算hash值的時(shí)間,而且同時(shí),由于新增的1bit是0還是1可以認(rèn)為是隨機(jī)的(hashCode里被作為索引的數(shù)往前走了一個(gè),走的這個(gè)可能是0,也可能是1),因此resize的過程,均勻的把之前的沖突的節(jié)點(diǎn)分散到新的bucket了。
6. HashMap使用
6.1 遍歷
用Iterator有兩種方式,分別把迭代器放到entry和keyset上,第一種更推薦,因?yàn)椴恍枰?code>get(key)
Iterator<Map.Entry<String, Integer>> entryIterator = map.entrySet().iterator();
while (entryIterator.hasNext()) {
Map.Entry<String, Integer> next = entryIterator.next();
System.out.println("key=" + next.getKey() + " value=" + next.getValue());
}
Iterator<String> iterator = map.keySet().iterator();
while (iterator.hasNext()){
String key = iterator.next();
System.out.println("key=" + key + " value=" + map.get(key));
}
7. 常見問題
7.1 樹形化閾值為何是8?
- 如果選擇6和8(如果鏈表小于等于6樹還原轉(zhuǎn)為鏈表,大于等于8轉(zhuǎn)為樹),中間有個(gè)差值7可以有效防止鏈表和樹頻繁轉(zhuǎn)換。假設(shè)一下,如果設(shè)計(jì)成鏈表個(gè)數(shù)超過8則鏈表轉(zhuǎn)換成樹結(jié)構(gòu),鏈表個(gè)數(shù)小于8則樹結(jié)構(gòu)轉(zhuǎn)換成鏈表,如果一個(gè)HashMap不停的插入、刪除元素,鏈表個(gè)數(shù)在8左右徘徊,就會(huì)頻繁的發(fā)生樹轉(zhuǎn)鏈表、鏈表轉(zhuǎn)樹,效率會(huì)很低。
- 還有一點(diǎn)重要的就是由于treeNodes的大小大約是常規(guī)節(jié)點(diǎn)的兩倍,因此我們僅在容器包含足夠的節(jié)點(diǎn)以保證使用時(shí)才使用它們,當(dāng)它們變得太?。ㄓ捎谝瞥蛘{(diào)整大?。r(shí),它們會(huì)被轉(zhuǎn)換回普通的node節(jié)點(diǎn),容器中節(jié)點(diǎn)分布在hash桶中的頻率遵循泊松分布,桶的長度超過8的概率非常非常小。所以作者應(yīng)該是根據(jù)概率統(tǒng)計(jì)而選擇了8作為閥值
7.2 key的選取
7.2.1 為什么Integer String等包裝類適合作為key
可減少哈希碰撞
- 因?yàn)?String、Integer 等包裝類是 final 類型的,具有不可變性,不可變性保證了計(jì)算 hashCode() 后鍵值的唯一性和緩存特性,不會(huì)出現(xiàn)放入和獲取時(shí)哈希碼不同的情況
- 包裝類已經(jīng)重寫了 equals() 和 hashCode() 方法,而這些方法是在存取時(shí)都會(huì)用到的。
讀取哈希值高效,此外官方實(shí)現(xiàn)的 equals() 和 hashCode() 都是嚴(yán)格遵守相關(guān)規(guī)范的,不會(huì)出現(xiàn)錯(cuò)誤。
7.2.2 Null可以做key嗎
可以,null的索引被設(shè)置為0,也就是Table[0]位置
在JDK7中,調(diào)用了putForNullKey()方法,處理空值
JDK8中,則修改了hash函數(shù),在hash函數(shù)中直接把key==null的元素hash值設(shè)為0,
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
//如果key為Null,直接把hash設(shè)為0
再通過計(jì)算索引的步驟
//(JDK11,函數(shù):putVal())
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
//索引:i = (n - 1) & hash
得到索引為0;
7.2.3 如何設(shè)計(jì)一個(gè)class作為key
- 重寫
equals方法,equals方法與hashCode()的關(guān)系保證正確(hashCode相同不一定equals,equals一定hashCode相同) - 讓這個(gè)類不可變
- 添加final修飾符,保證類不被繼承
- 保證所有成員變量必須私有,并且加上final修飾
- 不提供改變成員變量的方法,包括setter
- 通過構(gòu)造器初始化所有成員,進(jìn)行深拷貝(deep copy) ,傳入的構(gòu)造器參數(shù)也復(fù)制一份,而不是直接用引用
- 在getter方法中,不要直接返回對象本身,而是克隆對象,并返回對象的拷貝
7.3 為什么使用數(shù)組+鏈表的結(jié)構(gòu)
- 數(shù)組根據(jù)索引取值效率高,用來根據(jù)key確定桶的位置
- 鏈表用來解決hash沖突,索引值一樣時(shí),就在鏈表上增加一個(gè)節(jié)點(diǎn)。
7.3.1 為什么不用linkedList或者ArrayList代替數(shù)組
- 因?yàn)橛兴饕瑪?shù)組取值比LinkedList快;
- 數(shù)組是基本的數(shù)據(jù)結(jié)構(gòu),操作更方便(擴(kuò)容機(jī)制可以自定義,ArrayList擴(kuò)容是變成1.5倍容量)
7.4 hashmap中g(shù)et元素的過程?
對key的hashCode()做hash運(yùn)算,計(jì)算index; 如果在bucket里的第一個(gè)節(jié)點(diǎn)里直接命中,則直接返回; 如果有沖突,則通過key.equals(k)去查找對應(yīng)的Entry;
- 若為樹,則在樹中通過key.equals(k)查找,O(logn);
- 若為鏈表,則在鏈表中通過key.equals(k)查找,O(n)。
7.5 hashmap中put元素的過程?
調(diào)用putValue:
- 查看當(dāng)前
Table是否為空,為空就resize; - 查看對應(yīng)索引處是否為空,是就直接
newNode()放到Table[i]中
7.5 說說String中hashcode的實(shí)現(xiàn)?
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value;
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}
是以31為權(quán),每一位為字符的ASCII值進(jìn)行運(yùn)算,用int的自然溢出來等效取模。
假設(shè)String是ABC,
((A + 0*31) * 31 + 'B' )*31 + 'C'
7.5.1 為什么每次乘31?
- 31是個(gè)比較大的質(zhì)數(shù),適合用于hash
- 31 == 2^5 - 1,很適合改成位運(yùn)算,
i* 31 == (i << 5) - i
7.6 為什么不直接用紅黑樹
- 空間資源考慮:紅黑樹節(jié)點(diǎn)大小比鏈表節(jié)點(diǎn)大,占空間更多
- 時(shí)間資源:雖然查詢快,但是需要左右旋轉(zhuǎn),調(diào)整顏色等保持紅黑樹的性質(zhì),在節(jié)點(diǎn)比較少的情況下,節(jié)省下來的查詢時(shí)間開銷并不值。
7.7 HashMap在并發(fā)時(shí)有什么問題?1.8還有嗎?如何解決
多線程擴(kuò)容后,取元素時(shí)的死循環(huán)(1.8解決)
-
多線程put可能導(dǎo)致元素丟失(未解決)
元素為什么會(huì)丟失?
- 元素put時(shí)是一種“先查后改“的機(jī)制,即先計(jì)算得到索引,然后再往桶里面放;查詢可以同步進(jìn)行(假設(shè)有兩個(gè)索引相同的元素需要put,查詢出前一個(gè)節(jié)點(diǎn)都是A,那么插入時(shí),后插入的C可能就會(huì)覆蓋先插入的B)先插入的B就丟失了。
-
put函數(shù)中有一句++size表示HashMap當(dāng)前的Entry數(shù)量,但該操作不是原子的,源碼設(shè)計(jì)時(shí)就沒有考慮其并發(fā)安全性。在多線程執(zhí)行這句++size時(shí),結(jié)果很可能出錯(cuò),這導(dǎo)致元素個(gè)數(shù)是錯(cuò)的
put非null元素
get出來卻是null(未解決)
可以使用ConcurrentHashmap,Hashtable等線程安全等集合類。
7.9 map中的對象能否修改
Divenier總結(jié):
要看作為key的元素的類如何實(shí)現(xiàn)的,如果修改的部分導(dǎo)致其hashcode變化,則修改后不能get()到;
如修改部分對hashcode無影響,則可以修改。
因?yàn)?key 更新后 hashCode 也更新了,(這里是因?yàn)橹貙懥?hashcode 的原因)而 HashMap 里面的對象是我們原來哈希值的對象,在 get 時(shí)由于哈希值已經(jīng)變了,原來的對象不會(huì)被索引到了,所以結(jié)果為 null
因此當(dāng)把對象放到 HashMap 后就不要嘗試對 key 進(jìn)行修改操作,謹(jǐn)防出現(xiàn)哈希值變化或者 equals 比較不等的情況導(dǎo)致無法索引。
7.10 解決Hash沖突有幾個(gè)辦法?HashMap用的是什么辦法?
開放定址法、鏈地址法(拉鏈法)、再Hash法
HashMap用的是拉鏈法。
ThreadLocalMap是開放定址法
之所以采用不同的方式主要是因?yàn)?在 ThreadLocalMap中的散列值分散得十分均勻,很少會(huì)出現(xiàn)沖突。并且 ThreadLocalMap經(jīng)常需要清除無用的對象,使用純數(shù)組更加方便。
8. 并發(fā)安全
8.1 HashMap和HashTable
- 都實(shí)現(xiàn)了map接口,HashMap繼承自
AbstractMap,實(shí)現(xiàn)此接口比較簡單, HashTable繼承自Dictionary類(已廢棄),都是鍵值對存儲(chǔ)方式 - HashMap可以有Null鍵(位置是0),HashTable則不可以(直接返回 NullPointerException)
- HashMap線程不安全(非
synchronized) Table安全(幾乎所有的 public方法都加了synchronized,所以線程安全)
在 Collections 類中存在一個(gè)靜態(tài)方法:synchronizedMap(),該方法創(chuàng)建了一個(gè)線程安全的 Map 對象,并把它作為一個(gè)封裝的對象來返回。
部分參考資料:
https://tech.meituan.com/2016/06/24/java-hashmap.html