版權聲明:本文為博主原創文章,轉載請注明出處,歡迎交流學習!
HashMap在我們的工作中應用的非常廣泛,在工作面試中也經常會被問到,對於這樣一個重要的集合模型我們有必要弄清楚它的使用方法和它底層的實現原理。HashMap是通過key-value鍵值對的方式來存儲數據的,通過put、get方法實現鍵值對的快速存取,這是HashMap最基本的用法。HashMap底層是通過數組和鏈表相結合的混合結構來存放數據的。我們通過分析底層源碼來詳細了解一下HashMap的實現原理。
1、HashMap的初始化
在HashMap實例化時我們要了解兩個概念:初始容量和加載因子。HashMap是基於哈希表的Map接口實現,初始容量是哈希表在創建時的容量。加載因子是哈希表在其容量自動增加之前可以達到多滿的一種尺度。當哈希表中的條目數超過了加載因子與當前容量的乘積時,則要對該哈希表進行rehash操作(即重建內部數據結構),從而哈希表將具有大約兩倍於當前容量的新的容量。

以上是Java API中HashMap的構造方法,其源碼如下:
1 static final int DEFAULT_INITIAL_CAPACITY = 16;//默認初始容量16 2 static final int MAXIMUM_CAPACITY = 1 << 30;//定義最大容量 3 static final float DEFAULT_LOAD_FACTOR = 0.75f;//默認負載因子0.75 4 transient Entry[] table; 5 int threshold; //臨界值,值為容量與加載因子的乘積 6 final float loadFactor; //加載因子 7 8 public HashMap() { 9 this.loadFactor = DEFAULT_LOAD_FACTOR; 10 threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR); 11 table = new Entry[DEFAULT_INITIAL_CAPACITY]; 12 init(); 13 } 14 15 void init() { 16 }
以上構造方法定義了一個空的HashMap,其默認初始容量為16,默認初始加載因子為0.75,同時聲明了一個Entry類型的數組,數組初始長度為16。那么這里出現的Entry對象是如何定義的呢?看一下它的實現代碼:
1 static class Entry<K,V> implements Map.Entry<K,V> { 2 final K key; 3 V value; 4 Entry<K,V> next; //指向下一個Entry節點 5 final int hash; //哈希值 6 7 /** 8 * Creates new entry. 9 */ 10 Entry(int h, K k, V v, Entry<K,V> n) { 11 value = v; 12 next = n; 13 key = k; 14 hash = h; 15 } 16 17 public final K getKey() { 18 return key; 19 } 20 21 public final V getValue() { 22 return value; 23 } 24 25 public final V setValue(V newValue) { 26 V oldValue = value; 27 value = newValue; 28 return oldValue; 29 } 30 //重寫equals方法,判斷兩個Entry是否相等,如果兩個Entry對象的key和value相等,則返回true,否則返回false 31 public final boolean equals(Object o) { 32 if (!(o instanceof Map.Entry)) 33 return false; 34 Map.Entry e = (Map.Entry)o; 35 Object k1 = getKey(); 36 Object k2 = e.getKey(); 37 if (k1 == k2 || (k1 != null && k1.equals(k2))) { 38 Object v1 = getValue(); 39 Object v2 = e.getValue(); 40 if (v1 == v2 || (v1 != null && v1.equals(v2))) 41 return true; 42 } 43 return false; 44 } 45 //重寫hashCode方法,返回key的hashCode值與value的hashCode值異或運算所得的值 46 public final int hashCode() { 47 return (key==null ? 0 : key.hashCode()) ^ 48 (value==null ? 0 : value.hashCode()); 49 } 50 //重寫toString方法,返回此Entry對象的“key=value”映射關系 51 public final String toString() { 52 return getKey() + "=" + getValue(); 53 } 54 55 /** 56 * This method is invoked whenever the value in an entry is 57 * overwritten by an invocation of put(k,v) for a key k that's already 58 * in the HashMap. 59 */ 60 void recordAccess(HashMap<K,V> m) { //當向HashMap中添加鍵值對時,會調用此方法,這里方法體為空,即不做處理 61 } 62 63 /** 64 * This method is invoked whenever the entry is 65 * removed from the table. 66 */ 67 void recordRemoval(HashMap<K,V> m) { //當向HashMap中刪除鍵值對映射關系時,會調用此方法,這里方法體為空,即不做處理 68 } 69 }
Entry類是HashMap的內部類,其實現了Map.Entry接口。Entry類里定義了4個屬性:Object類型的key、value(K、V類型可以看成Object類型),Entry類型的next屬性(這個next其實就是一個指向下一個Entry對象的引用,形成了一個鏈表,通過此Entry對象的next屬性可以找到其下一個Entry對象)和int型的hash值。HashMap底層維護的就是一個個Entry對象。在Entry類里還重寫了equals方法,若兩個Entry的key和value都相等,則返回true,否則返回false,同時還重寫了hashCode方法。
2、HashMap的底層數據結構
前面提到過HashMap的底層是基於數組和鏈表來實現的,那么如何決定一個Entry對象是存放在數組中的哪個位置的呢?它是通過計算hash值來決定存儲位置的,同時在查找元素的時候同樣也是計算出一個值來找到對應的位置,因此它具有相當快的查詢速度。HashMap是根據key的hashCode值來計算hash值的,相同的hashCode值計算出來的hash值也是相同的。當存儲的對象達到了一定數量,就有可能出現不同對象的key的hashCode值是相同的,因此計算出來的hash值也相同,這樣就出現了沖突。哈希沖突的解決方法有很多,比如再哈希法,這種方法是同時構造多個不同的哈希函數,當發生沖突時就換另外的函數重新計算hash值,直到不再產生沖突為止。HashMap是通過單鏈表來解決哈希沖突的,這種方法也被稱為拉鏈法。如圖所示:

在上圖中,左邊的部分是哈希表(也稱為哈希數組),右邊是一個單鏈表,單鏈表是用來解決哈希沖突的,數組里的每一個元素都是一個單鏈表的頭節點,當不同的key計算出的數組中的存放位置相同時,就將此對象添加到單鏈表中。
3、數據存儲
在HashMap中定義了put方法來向集合中添加數據,數據以鍵值對的形式存儲,put方法的實現如下:
1 public V put(K key, V value) { 2 //如果存入HashMap的key為null,則將該鍵值對添加到table[0]中 3 if (key == null) 4 return putForNullKey(value); 5 //key不為null,調用hash方法計算key的hashCode值對應的hash值 6 int hash = hash(key.hashCode()); 7 //根據計算出的hash值,結合數組的長度計算出數組中的插入位置i 8 int i = indexFor(hash, table.length); 9 //遍歷數組下標為i處的鏈表,如果鏈表上存在元素,其hash值與上述計算得到的hash值相等, 10 //並且其key值與新增的鍵值對的key值相等,那么就以新增鍵值對的value替換此元素的value值, 11 //並返回此元素原來的value 12 for (Entry<K,V> e = table[i]; e != null; e = e.next) { 13 Object k; 14 if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { 15 V oldValue = e.value; 16 e.value = value; 17 e.recordAccess(this); 18 return oldValue; 19 } 20 } 21 22 modCount++; //操作次數加1 23 //如果鏈表上不存在滿足條件的元素,則將鍵值對對應生成的Entry對象添加到table[i]處, 24 //並將下標為i處原先的Entry對象鏈接到新的Entry對象后面 25 addEntry(hash, key, value, i); 26 return null; 27 } 28 29 30 31 private V putForNullKey(V value) { 32 //遍歷數組下標為0處的鏈表,如果鏈表中存在元素其key為null,則用value覆蓋此元素原來的value 33 for (Entry<K,V> e = table[0]; e != null; e = e.next) { 34 if (e.key == null) { 35 V oldValue = e.value; 36 e.value = value; 37 e.recordAccess(this); 38 return oldValue; 39 } 40 } 41 modCount++; //操作數加1 42 //如果鏈表中不存在滿足條件的元素,則將此鍵值對生成的Entry對象存放到table[0] 43 addEntry(0, null, value, 0);//key為null,計算出的hash值為0 44 return null; 45 } 46 47 48 //計算hash值 49 static int hash(int h) { 50 // This function ensures that hashCodes that differ only by 51 // constant multiples at each bit position have a bounded 52 // number of collisions (approximately 8 at default load factor). 53 h ^= (h >>> 20) ^ (h >>> 12); 54 return h ^ (h >>> 7) ^ (h >>> 4); 55 } 56 57 58 59 //根據hash值和數組長度,計算出在數組中的索引位置 60 static int indexFor(int h, int length) { 61 return h & (length-1); //計算出的值不會超出數組的長度 62 } 63 64 65 66 void addEntry(int hash, K key, V value, int bucketIndex) { 67 //獲取table[bucketIndex]處的Entry對象 68 Entry<K,V> e = table[bucketIndex]; 69 //根據key-value生成新的Entry對象,並將新的Entry對象存入table[bucketIndex]處,將其next引用指向原來的對象 70 table[bucketIndex] = new Entry<K,V>(hash, key, value, e); 71 //如果數組容量大於或等於臨界值,則進行擴容 72 if (size++ >= threshold) 73 resize(2 * table.length); //容量為原來的2倍 74 }
以上就是put方法的實現原理,我給出了詳細的代碼注釋。上面已經講到過HashMap底層的數據結構是由數組和單向鏈表構成的,當我們向HashMap中put一對key-value鍵值對時,首先判斷key是否為null,如果為null,則遍歷table[0]處的鏈表,若此鏈表上存在key為null的元素,則用value覆蓋此元素的value值,如果不存在這樣的元素,那么將此鍵值對生成的Entry對象存放到table[0]中;如果key不為null,首先根據key的hashCode值計算出hash值,根據hash值和數組長度計算出要存放到數組中的位置i,然后遍歷table[i]處的鏈表,如果鏈表上存在元素其hash值與計算得到的hash值相等並且其key值與新增的key相等,那么就以新增的value覆蓋此元素原來的value並返回原來的value值;如果鏈表上不存在滿足上面條件的元素,則將key-value生成的Entry對象存放到table[i]處,並將其next指向此處原來的Entry對象。這樣經過多次put操作,就構成了數組加鏈表的存儲結構。
4、數據讀取
HashMap的get方法可以根據key返回其對應的value,如果key為null,則返回null。
1 public V get(Object key) { 2 //如果key為null,則循環table[0]處的單鏈表 3 if (key == null) 4 return getForNullKey(); 5 //key不為null,根據key的hashCode計算出一個hash值 6 int hash = hash(key.hashCode()); 7 //根據hash值和數組長度計算出一個數組下標值,並且遍歷此下標處的單鏈表 8 for (Entry<K,V> e = table[indexFor(hash, table.length)]; 9 e != null; 10 e = e.next) { 11 Object k; 12 //如果Entry對象的hash值跟上面計算得到的hash值相等,並且key也相等,那么就返回此Entry對象value 13 if (e.hash == hash && ((k = e.key) == key || key.equals(k))) 14 return e.value; 15 } 16 //如果單鏈表上不存在滿足上述條件的Entry對象,則表明HashMap不包含該key的映射關系,返回null 17 return null; 18 } 19 20 21 22 private V getForNullKey() { 23 //獲取table[0]處的Entry對象,並循環其鏈接的單鏈表,如果單鏈表上存在不為null的對象, 24 //並且其key為null,那么就返回此對象的value 25 for (Entry<K,V> e = table[0]; e != null; e = e.next) { 26 if (e.key == null) 27 return e.value; 28 } 29 //如果單鏈表上不存在滿足條件的對象,則返回null 30 return null; 31 }
了解了put方法的原理,我們就不難理解get的實現原理了,與之類似也是要根據key的hashCode值來計算出一個hash值,然后根據hash值和數組長度計算出一個數組下標值,接着循環遍歷此下標處的單鏈表,尋找滿足條件的Entry對象並返回value,此value就是HashMap中該key所映射的value。注意分析當key為null時的情況:如果HashMap中有key為null的映射關系,那么就返回null映射的value,否則就表明HashMap中不存在key為null的映射關系,返回null。同理,當get方法返回的值為null時,並不一定表明該映射不包含該鍵的映射關系,也可能是該映射將該鍵顯示的映射為null,即put(key, null)。可使用containKey方法來區分這兩種情況。
5、移除映射關系
remove方法根據指定的key從HashMap映射中移除相應的映射關系(如果存在),此方法返回一個value。
1 public V remove(Object key) { 2 Entry<K,V> e = removeEntryForKey(key); 3 return (e == null ? null : e.value); 4 } 5 6 7 8 final Entry<K,V> removeEntryForKey(Object key) { 9 //根據key的hashCode計算hash值 10 int hash = (key == null) ? 0 : hash(key.hashCode()); 11 //根據hash值和數組長度計算數組下標值i 12 int i = indexFor(hash, table.length); 13 //獲取下標為i處的數組元素 14 Entry<K,V> prev = table[i]; 15 Entry<K,V> e = prev; 16 //遍歷數組下標為i處的單鏈表 17 while (e != null) { 18 Entry<K,V> next = e.next; 19 Object k; 20 //如果此單鏈表上存在Entry對象e,其hash值與計算出的hash值相等並且其key也跟傳入的key"相等",則從單鏈表上移除e 21 if (e.hash == hash && 22 ((k = e.key) == key || (key != null && key.equals(k)))) { 23 modCount++; 24 size--; //map中的映射數減1 25 //判斷滿足條件的Entry對象是在數組下標i處還是在數組外面的單鏈表上 26 if (prev == e) 27 table[i] = next; 28 else 29 prev.next = next; 30 e.recordRemoval(this); 31 return e; 32 } 33 prev = e; 34 e = next; 35 } 36 37 return e; 38 }
從上面的源碼可以看出,remove方法的原理是先找出滿足條件的Entry對象,然后從單鏈表上刪除該對象,並返回該對象中的value,本質上是對單鏈表的操作。
6、總結
從以上源碼的分析中我們知道了HashMap底層維護的是數組加鏈表的混合結構,這是HashMap的核心,只要掌握了這一點我們就能很容易弄清楚HashMap中映射關系的各種操作原理,其本質是對數組和鏈表的操作。要注意的是HashMap不是線程安全的,我們可以使用Collections.synchoronizedMap方法來獲得線程安全的HashMap。例如:
Map map = Collections.sychronizedMap(new HashMap());
以上是我個人對HashMap底層原理的一點理解,不妥的地方歡迎指正!
