HashMap的key可以是可變的對象嗎???
HashMap的key可以是可變的對象嗎???
大家都知道,HashMap的是key-value(鍵值對)組成的,這個key既可以是基本數據類型對象,如Integer,Float,同時也可以是自己編寫的對象,那麽問題來了,這個作為key的對象是否能夠改變呢?或者說key能否是一個可變的對象?如果可以該HashMap會怎麽樣?
可變對象
可變對象是指創建後自身狀態能改變的對象。換句話說,可變對象是該對象在創建後它的哈希值(由類的hashCode()方法可以得出哈希值)可能被改變。
為了能直觀的看出哈希值的改變,下面編寫了一個類,同時重寫了該類的hashCode()方法和它的equals()方法【至於為什麽要重寫equals方法可以看博客:http://www.cnblogs.com/0201zcr/p/4769108.html】,在查找和添加(put方法)的時候都會用到equals方法。
在下面的代碼中,對象MutableKey的鍵在創建時變量 i=10 j=20,哈希值是1291。
然後我們改變實例的變量值,該對象的鍵 i 和 j 從10和20分別改變成30和40。現在Key的哈希值已經變成1931。
顯然,這個對象的鍵在創建後發生了改變。所以類MutableKey是可變的。
讓我們看看下面的示例代碼:
public class MutableKey { private int i; private int j; public MutableKey(int i, int j) { this.i = i; this.j = j; } public final int getI() { return i; } public final void setI(int i) { this.i = i; } public final int getJ() { return j; } public final void setJ(int j) { this.j = j; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + i; result = prime * result + j; return result; } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (!(obj instanceof MutableKey)) { return false; } MutableKey other = (MutableKey) obj; if (i != other.i) { return false; } if (j != other.j) { return false; } return true; } }
測試:
public class MutableDemo { public static void main(String[] args) { // Object created MutableKey key = new MutableKey(10, 20); System.out.println("Hash code: " + key.hashCode()); // Object State is changed after object creation. key.setI(30); key.setJ(40); System.out.println("Hash code: " + key.hashCode()); } }
結果:
Hash code: 1291
Hash code: 1931
只要MutableKey 對象的成員變量i或者j改變了,那麽該對象的哈希值改變了,所以該對象是一個可變的對象。
HashMap如何存儲鍵值對
HashMap底層是使用Entry對象數組存儲的,而Entry是一個單項的鏈表。當調用一個put()方法將一個鍵值對添加進來是,先使用hash()函數獲取該對象的hash值,然後調用indexFor方法查找到該對象在數組中應該存儲的下標,假如該位置為空,就將value值插入,如果該下標出不為空,則要遍歷該下標上面的對象,使用equals方法進行判斷,如果遇到equals()方法返回真的則進行替換,否則將其插入,源碼詳解可看:http://www.cnblogs.com/0201zcr/p/4769108.html。
查找時只需要查詢通過key值獲取獲取hash值,然後找到其下標,遍歷該下標下面的Entry對象即可查找到value。【具體看下面源碼及其解釋】
在HashMap中使用可變對象作為Key帶來的問題
如果HashMap Key的哈希值在存儲鍵值對後發生改變,Map可能再也查找不到這個Entry了。
public V get(Object key) { // 如果 key 是 null,調用 getForNullKey 取出對應的 value if (key == null) return getForNullKey(); // 根據該 key 的 hashCode 值計算它的 hash 碼 int hash = hash(key.hashCode()); // 直接取出 table 數組中指定索引處的值, for (Entry<K,V> e = table[indexFor(hash, table.length)]; e != null; // 搜索該 Entry 鏈的下一個 Entr e = e.next) // ① { Object k; // 如果該 Entry 的 key 與被搜索 key 相同 if (e.hash == hash && ((k = e.key) == key || key.equals(k))) return e.value; } return null; }
上面是HashMap的get()方法源碼,通過上面我們可以知道,如果 HashMap 的每個 bucket 裏只有一個 Entry 時,HashMap 可以根據索引、快速地取出該 bucket 裏的 Entry;在發生“Hash 沖突”的情況下,單個 bucket 裏存儲的不是一個 Entry,而是一個 Entry 鏈,系統只能必須按順序遍歷每個 Entry,直到找到想搜索的 Entry 為止——如果恰好要搜索的 Entry 位於該 Entry 鏈的最末端(該 Entry 是最早放入該 bucket 中),那系統必須循環到最後才能找到該元素。
同時我們也看到,判斷是否找到該對象,我們還需要判斷他的哈希值是否相同,假如哈希值不相同,根本就找不到我們要找的值。
如果Key對象是可變的,那麽Key的哈希值就可能改變。在HashMap中可變對象作為Key會造成數據丟失。
下面的例子將會向你展示HashMap中有可變對象作為Key帶來的問題。
import java.util.HashMap; import java.util.Map; public class MutableDemo1 { public static void main(String[] args) { // HashMap Map<MutableKey, String> map = new HashMap<>(); // Object created MutableKey key = new MutableKey(10, 20); // Insert entry. map.put(key, "Robin"); // This line will print ‘Robin‘ System.out.println(map.get(key)); // Object State is changed after object creation. // i.e. Object hash code will be changed. key.setI(30); // This line will print null as Map would be unable to retrieve the // entry. System.out.println(map.get(key)); } }
輸出:
Robin null
如何解決
在HashMap中使用不可變對象。在HashMap中,使用String、Integer等不可變類型用作Key是非常明智的。
我們也能定義屬於自己的不可變類。
如果可變對象在HashMap中被用作鍵,那就要小心在改變對象狀態的時候,不要改變它的哈希值了。我們只需要保證成員變量的改變能保證該對象的哈希值不變即可。
在下面的Employee示例類中,哈希值是用實例變量id來計算的。一旦Employee的對象被創建,id的值就不能再改變。只有name可以改變,但name不能用來計算哈希值。所以,一旦Employee對象被創建,它的哈希值不會改變。所以Employee在HashMap中用作Key是安全的。
import java.util.HashMap; import java.util.Map; public class MutableSafeKeyDemo { public static void main(String[] args) { Employee emp = new Employee(2); emp.setName("Robin"); // Put object in HashMap. Map<Employee, String> map = new HashMap<>(); map.put(emp, "Showbasky"); System.out.println(map.get(emp)); // Change Employee name. Change in ‘name‘ has no effect // on hash code. emp.setName("Lily"); System.out.println(map.get(emp)); } } class Employee { // It is specified while object creation. // Cannot be changed once object is created. No setter for this field. private int id; private String name; public Employee(final int id) { this.id = id; } public final String getName() { return name; } public final void setName(final String name) { this.name = name; } public int getId() { return id; } // Hash code depends only on ‘id‘ which cannot be // changed once object is created. So hash code will not change // on object‘s state change @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + id; return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; Employee other = (Employee) obj; if (id != other.id) return false; return true; } }
輸出
Showbasky Showbasky
致謝:感謝您的耐心閱讀!
本文翻譯自 Coding Geek, 原文地址。英文水平有限,有些地方翻譯得不太精確
絕大多數Java開發者都在使用Map類,尤其是HashMap。HashMap是一種簡單易用且強大的存取數據的方法。但是,有多少人知道HashMap內部是如何工作的?幾天前,為了對這個基本的數據結構有深入的了解,我閱讀大量的HashMap源碼(開始是Java7,然後是Java8)。在這篇文章裏,我會解釋HashMap的實現,介紹Java8的新實現,聊一聊性能,內存,還有使用HashMap時已知的一些問題。
內部存儲
HashMap 類實現了Map<k,v>
接口,這個接口的基本主要方法有:
V put(K key, V value)
V get(Object key)
V remove(Object key)
Boolean containsKey(Object key)
HashMap使用了內部類Entry<k,v>
來存儲數據,這個類是一個帶有兩個額外數據的簡單 鍵-值對 結構:
- 一個是另一個
Entry<k,v>
的引用,這樣HashMap可以像單獨的鏈表一樣存儲數據 - 一個
hash
值,代表了key的哈希值,避免了HashMap每次需要的時候再來計算
下面是Java7裏Entry的部分實現:
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
int hash;
…
}
HashMap存儲數據到多個單獨的entry鏈表裏,所有的鏈表都登記到一個Entry數組裏(Entry<K,V>[] array
),並且這個內部數組默認容量是16。
下面的圖片展示了一個HashMap實例的內部存儲,一個可為null的Entry數組,每一個Entry都可以鏈接到另一個Entry來形成一個鏈表:
所有具有相同哈希值的key都會放到同一個鏈表裏,具有不同哈希值的key最終也有可能在同一個鏈表裏。
當調用 put(K key, V value)
或者get(Object key)
這些方法時,會先計算這個Entry應該存放的鏈表在內部數組中的索引(index),然後方法會叠代整個鏈表來尋找具有相同key的Entry(使用key的 equals()
方法)
get()
方法,會返回這個Entry關聯的value值(如果Entry存在)put(K key, V value)
方法,如果Entry存在則重置value值,如果不存在,則以key,value參數構造一個Entry並插入到鏈表的頭部。
獲取鏈表在數組內的索引通過三個步驟確定:
- 首先獲取Key的哈希值
- 對哈希值再次進行哈希運算,避免出現一個很差的哈希算法,把所有的數據放到內部數組的同一個鏈表裏
- 對再次哈希的哈希值進行數組長度(最小為1)的位掩碼運算,這個運算保證生成的索引不會比數組的長度大,你可以把它當成一個優化過的取模運算
下面是Java7 和 Java8處理索引的源代碼:
// the "rehash" function in JAVA 7 that takes the hashcode of the key
static int hash(int h) {
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
// the "rehash" function in JAVA 8 that directly takes the key
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
// the function that returns the index from the rehashed hash
static int indexFor(int h, int length) {
return h & (length-1);
}
為了更高效的運作,內部數組的大小必須是2的指數大小,讓我們來看看這是為什麽。
想象一下數組大小是17,掩碼值就是16(size-1),16的二進制表示是 0…010000
,那麽對於任何哈希值H通過位運算H AND 16
得到的索引就只會是16或者0,這意味著17大小的鏈表數組只會使用到兩個:索引為0的和索引為16的,非常浪費。
但是,如果你取2的指數大小例如16,位運算是 H AND 15
,15的二進制表示是 0…001111
, 那麽取索引的運算就會輸出0~15之間的值,大小16的數據就能完全使用到。舉例:
- 如果
H = 952
二進制表示為 0..0111011 1000, 相關的索引就是0…01000 = 8
- 如果
H = 1576
二進制表示為 0..01100010 1000, 相關的索引就是0…01000 = 8
- 如果
H = 12356146
二進制表示為 010111100100010100011 0010, 相關的索引就是0…00010 = 2
- 如果
H = 59843
二進制表示為 0111010011100 0011, 相關的索引就是0…00011 = 3
這就是為什麽數組的大小必須是2的指數大小,這個機制對開發人員是透明的,如果選擇了一個37大小的HashMap,那麽Map會自動選擇37之後的一個2的指數大小(64)來做為內部數組的容量。
自動調整大小
我們獲取到索引之後,函數(put,get或者remove) 訪問/叠代 關聯的鏈表,檢查是否有指定key對應的Entry。 不做改動的話,這個機制會帶來性能問題,因為這個函數會遍歷整個鏈表來檢查Entry是否存在。
想象一下如果內部數組大小是初始值16,我們有兩百萬條數據需要存儲,最好的情況下, 每個鏈表裏平均有 125 000個數據(2000000/16).因此,每個get(),remove(),put()
會導致125 000個叠代或者操作。為了避免出現這種情況,HashMap會自動調整它的內部數組大小來保持每個鏈表盡可能的短。
當你創建一個HashMap時,你可以指定一個初始化大小和一個載入因數:
public HashMap(int initialCapacity, float loadFactor)
如果不指定參數,缺省的initialCapacity
是16,loadFactor
是0.75,initialCapacity
即代表了Map內部數組的大小。
每次當你調用put()
方法加入一個新的Entry時,這個方法會檢測是否需要增加內部數組大小,因此map存儲了兩個數據:
- map的大小,代表了HashMap裏 Entry的數量,每次新增或者移除Entry時都會更新這個值
- 一個閾值: 內部數組大小 * 載入因數 ,每次自動調整大小後都會刷新
添加一個新Entry時,put函數會檢查 map的大小 是否大於閾值 ,如果大於,則會創建一個雙倍大小的數組,當新數組的大小改變,索引計算函數(返回 哈希值 & (數組大小-1) 的位運算)也會跟著改變。因此,數組的重新調整新建了兩倍數量的鏈表,並且 重新分發現有的Entry到這些數組內(註:原文括號有下面一句補充,暫時不明白是什麽意思。看HashMap的源代碼,是所有的數據分發到新的數組內,舊的直接棄用)
(the old ones and the newly created).
自動調整的目的是減少鏈表的長度從而減小 put(),remove(),get()
等函數的時間開銷,所有具有相同哈希值的Entry在重新調整大小後還會在同一個鏈表內,原來在同一個鏈表內具有不同哈希值的Entry則有可能不在同一個鏈表內了。
上面這個圖展示了一個HashMap自動調整前後的情況,在調整前,為了拿到Entry E,必須要叠代5次,調整後,只需要兩次。速度快了兩倍!
註意:HashMap只會增加內部數組的大小,沒有提供方法變小。
線程安全
如果你已經了解過HashMap,你知道它不是線程安全的,但是有沒有想過為什麽?
想象一下這種場景:你有一個寫線程只往Map裏寫新數據,還有一個讀線程只往裏讀數據,為什麽不能很好的運作?
因為在重新調整內部數組大小的時候,如果線程正在寫或者取對象,Map可能會使用調整前的索引,這樣就找不到調整後的Entry所在的位置了。
最壞的情況是:兩個線程同時往裏面放數據,同時調用了調整內部數組大小的方法。當兩個線程都在修改鏈表時,Map其中的某個鏈表可能會陷入一個內部循環,如果你試圖在這個鏈表裏取數據時,可能會永遠取不到值。
HashTable 為了避免這種情況,做了線程安全的實現。但是,所有的CRUD方法都是 同步阻塞的,所以會很慢。例如,線程1調用get(key1)
,線程2調用get(key2)
,線程3調用get(key3)
,同一時間只會有一個線程能拿到值,即使他們本來可以同時獲取這三個值。
其實從Java5開始就有一個更高效的線程安全的HashMap的實現了:ConcurrentHashMap。只有鏈表是同步阻塞的,因此多線程可以同時get,put,或者remove數據,只要沒有訪問同一個鏈表或者重新調整內部數組大小就行。在多線程應用裏,使用這種實現顯然會更好。
key的不變性
為什麽字符串和整數是HashMap的Key的一種很好的實現呢? 大多是因為他們的不變性。如果你選擇自己新建一個Key類並且不保證它的不變性的話,在HashMap裏面可能就會丟失數據,讓我們來看下面一種使用情況:
- 你有一個key,內部值是1
- 你用這個key往HashMap裏存了一個數據
- HashMap從這個key的哈希碼裏生成了一個哈希值(就是從1的哈希碼獲取)
- Map在最近創建的Entry裏存儲了這個哈希值
- 你把key的內部值改成2
- key的哈希碼改變了但是HashMap不知道(因為已經存了舊的哈希值)
- 你想要用改變後的key獲取數據
- Map會計算你的key的新哈希碼,來定位到數據位於哪個鏈表:
- 情況1:你已經改了你的key,map試圖從錯誤的鏈表裏尋找數據,當然找不到
- 情況2:你很幸運!改變後的key生成的索引和改變前一樣,map遍歷整個鏈表尋找具有相同key的Entry。但是為了匹配key,map先會匹配key的哈希值然後調用equals()方法來對照。因為你改變後的key哈希值也已經變了,map最終也找不到相應的Entry (註:應該也有可能找到錯誤的數據出來)
這裏有一個具體的例子,我存了兩個鍵值對到Map裏,我修改了第一個key並且試圖拿出這兩個值,只有第二個值有返回,第一個值已經丟失在Map裏:
public class MutableKeyTest {
public static void main(String[] args) {
class MyKey {
Integer i;
public void setI(Integer i) {
this.i = i;
}
public MyKey(Integer i) {
this.i = i;
}
@Override
public int hashCode() {
return i;
}
@Override
public boolean equals(Object obj) {
if (obj instanceof MyKey) {
return i.equals(((MyKey) obj).i);
} else
return false;
}
}
Map<MyKey, String> myMap = new HashMap<>();
MyKey key1 = new MyKey(1);
MyKey key2 = new MyKey(2);
myMap.put(key1, "test " + 1);
myMap.put(key2, "test " + 2);
// modifying key1
key1.setI(3);
String test1 = myMap.get(key1);
String test2 = myMap.get(key2);
System.out.println("test1= " + test1 + " test2=" + test2);
}
}
輸出結果是test1= null test2=test 2
,和預期的一樣,Map用改變後的key1找不回第一個字符串。
JAVA8的改進
Java8裏,HashMap的內部表示已經改變了很多了。的確,Java7裏HashMap的實現有1K行代碼,而Java8裏有2K。我前面所說的大部分都是真的,除了Entry鏈表。在Java8裏,仍然存在一個內部數組不過裏面存儲的都是節點(Node),但是節點包含的信息和Entry完全一樣,因為也可以看做鏈表,下面是Java8裏節點實現的部分代碼:
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
那麽對比Java7最大的變化是什麽呢?節點(Nodes)可以被樹節點(TreeNodes)繼承。樹節點是一種紅黑樹的數據結構,存儲了更多信息,可以讓你以O(log(n))
的算法復雜度新增,刪除或者是獲取一個元素。
下面是一個樹節點內存儲的數據的詳細列表供參考:
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
final int hash; // inherited from Node<K,V>
final K key; // inherited from Node<K,V>
V value; // inherited from Node<K,V>
Node<K,V> next; // inherited from Node<K,V>
Entry<K,V> before, after;// inherited from LinkedHashMap.Entry<K,V>
TreeNode<K,V> parent;
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev;
boolean red;
紅黑樹是一種自平衡的二分搜索樹。它的內部機制確定了不管是新增還是移除節點,長度永遠在log(n)內。使用這種樹的一個主要優點是,當一個內部表有許多相同的數據在同一個容器內時,在樹中搜索會花費O(log(n))
的時間復雜度,而鏈表會花費log(n)
。
如你所見,樹比鏈表占用了更多的空間(我們稍後會談到這個)。
通過繼承,內部表可以包含 節點(鏈表) 和 樹節點(紅黑樹)兩種節點。Oracle通過下面的規則,決定同時使用這兩種數據結構:
- 如果一個內部表的索引超過8個節點,鏈表會轉化為紅黑樹
- 如果內部表的索引少於6個節點,樹會變回鏈表
上圖展示了一個Java8 HashMap的內部數組的結構,具有樹(桶0),和鏈表(桶1,2,3) ,桶0因為有超過8個節點所以結構是樹。
內存開銷
JAVA7
使用HashMap會帶來一定的內存開銷,在Java7裏,一個HashMap用Entry包含了 許多鍵值對,一個Entry裏會有:
- 下一個entry的引用
- 一個預計算好的哈希值(整型)
- 一個key的引用
- 一個value的引用
此外,Java7裏 HashMap使用一個 Entry的內部數組。假設 一個HashMap包含了N個元素,內部數組容量是 C, 額外內存開銷約為:
sizeOf(integer) * N + sizeOf(reference) * (3 * N +C)
- 一個整數是 4 字節
- 一個引用的大小取決於 JVM/OS/Precessor 不過通常也是4字節
小貼士:從JAVA7起,HashMap類初始化的方法是懶惰的,這意味著即使你分配了一個HashMap,內部Entry數組在內存裏也不會分配到空間( 4 * 數組大小 個字節),直到你調用第一個put()方法
JAVA8
java8的實現裏,獲取內存用量變得稍微復雜了一點。因為 Entry 和 樹節點包含的數據是一樣的,但是樹節點會多6個引用和1個布爾值。
如果全部都是 普通鏈表節點,那麽內存用量和java7一樣。
如果全部都是 樹節點,內存用量變成:N * sizeOf(integer) + N * sizeOf(boolean) + sizeOf(reference)* (9*N+CAPACITY )
在大多數標準的 JVM裏,這個式子等於 44 * N + 4 * CAPACITY
字節
性能問題
傾斜HashMap和平衡HashMap
最好的情況下,get/put方法只有 O(1)的時間復雜度。但是,如果你不關心key的哈希函數,調用put/get/方法可能會非常慢。
put/get的良好性能取決於如何分配數據到內部數組不同的索引。如果key的哈希函數設計不良,你會得到一個傾斜的HashMap(和內部數組大小無關)。所有在最長鏈表上的put/get會非常慢,因為會遍歷整個鏈表。最壞的情況下(所有數據都在同一個索引下), 時間復雜度是O(n).
下面是一個例子,第一個圖片展示了一個傾斜HashMap,第二個圖則是一個平衡的HashMap:
這個傾斜HashMap在索引0上的get/put非常耗時,獲取Entry K會進行6次叠代
在這個平衡HashMap內,獲取Entry K只要進行3次叠代。這兩個HashMap存儲的數據量相同,內部數組大小也一樣。唯一的區別,就是分發數據的key的哈希函數。
下面是一個極端的例子,我創建了一個哈希函數,把兩百萬的數據都放到同一個數組索引下:
public class Test {
public static void main(String[] args) {
class MyKey {
Integer i;
public MyKey(Integer i){
this.i =i;
}
@Override
public int hashCode() {
return 1;
}
@Override
public boolean equals(Object obj) {
…
}
}
Date begin = new Date();
Map <MyKey,String> myMap= new HashMap<>(2_500_000,1);
for (int i=0;i<2_000_000;i++){
myMap.put( new MyKey(i), "test "+i);
}
Date end = new Date();
System.out.println("Duration (ms) "+ (end.getTime()-begin.getTime()));
}
}
在我的機器上(core i5-2500k @ 3.6Ghz
),這個程序跑了超過45分鐘(java 8u40
),45分鐘後我中斷了這個程序。
現在,我運行相同的代碼,只是使用下面的哈希函數:
@Override
public int hashCode() {
int key = 2097152-1;
return key+2097152*i;
}
結果只花了 46秒 !! 這個哈希函數比先前那一個有一個更好的數據分發所以put函數運行快得多。
如果我還是運行這段代碼,但是換成下面這個更好的哈希函數:
@Override
public int hashCode() {
return i;
}
現在,程序只需要2秒。
我希望你意識到哈希函數有多麽重要。如果上面的測試在java7上運行,第一個和第二個測試的性能甚至還會更差(java7的復雜度是 O(n),java8是 O(log(n)))
當你使用HashMap時,你需要找到一個哈希函數,可以 把key分發到盡量多的索引上,為了做到這一點,你需要避免哈希碰撞。字符串是不錯的一種key,因為它有 很不錯的哈希函數。整數做key也不錯,因為它的哈希函數就是本身的值。
重設大小的開銷
如果你需要存儲大量數據,你應該在創建HashMap時設置一個接近你預期值的初始化大小。如果你不這麽做,map會用默認的 16數組大小和0.75的 載入因數。 前面11個put會很快但是第12個(16*0.75)會創建一個容量為32的新數組,第13~23個put也會很快但是第24個會再次創建一個雙倍大小的數組。這個內部重設大小的操作會出現在第48次,96次,192次……。在數據量較小時,這個操作很快,但是當數據量增大時,這個操作會費時數秒到數分鐘不等。通過指定預期初始化大小,你可以避免這些操作開銷。
但是這也有一個弊端,如果你設置了一個很大的數組大小像 2^28
而你只用了2^26
,你會浪費掉大量的內存(這個例子裏大約是 2^30 字節)
總結
對於簡單的使用,你不需要知道HashMap是如何工作的,因為你感覺不出 O(1)、O(n)、O(log(n))的區別。但是了解這種最常用的數據結果的底層機制總是有好處的,何況,對於java開發者來說,這是一個很典型的面試問題。在大數據量時,知道它是如果工作的,知道哈希函數的重要性 就變得非常重要了。
希望這篇文章能幫助你加深對HashMap實現細節的了解。
HashMap的key可以是可變的對象嗎???