從JDK 1.7到JDK 1.8,HashMap在底層實現上發生了顯著的變化,
主要體現在數據結構、鏈表插入方式、哈希算法、擴容機制以及并發性方面。
以下是具體的變化點:
1. 數據結構的變化
- JDK 1.7:HashMap的底層數據結構是數組+單向鏈表。當哈希沖突發生時,新的元素會插入到鏈表的頭部(頭插法)。
- JDK 1.8:HashMap的底層數據結構變為數組+鏈表/紅黑樹。當鏈表長度超過一定閾值(默認為8)時,鏈表會轉換成紅黑樹,以提高查詢效率。這種變化使得HashMap在處理大量數據時能夠保持較好的性能。
2. 鏈表插入方式的變化
- JDK 1.7:鏈表插入使用的是頭插法,即新的元素會被插入到鏈表的頭部。
- JDK 1.8:鏈表插入使用的是尾插法。這是因為JDK 1.8在插入元素時需要判斷鏈表長度,尾插法便于統計鏈表元素個數。同時,尾插法也避免了頭插法可能導致的鏈表反轉問題。
3. 哈希算法的變化
- JDK 1.7:哈希算法相對復雜,涉及多種右移和位運算操作。
- JDK 1.8:哈希算法進行了簡化。這是因為JDK 1.8引入了紅黑樹,可以在一定程度上彌補簡化哈希算法可能帶來的散列性降低問題,從而節省CPU資源。
4. 擴容機制的變化
- JDK 1.7:擴容時,會重新創建一個更大的數組,并將原數組中的元素逐個添加到新數組中。這個過程比較耗費時間和性能。
- JDK 1.8:擴容機制得到了優化。當達到擴容條件時(容量*負載因子超過閾值),會創建一個大小為原數組兩倍的新數組,并采用更高效的方式遷移數據。此外,JDK 1.8還引入了“樹化”的概念,即在擴容時,鏈表長度達到閾值的桶會直接轉換為紅黑樹,以減少擴容操作的時間復雜度。
5. 并發性的改進
- JDK 1.7:HashMap本身是非線程安全的,在多線程環境下使用時需要額外的同步機制來保證數據一致性。
- JDK 1.8:雖然HashMap本身仍然是非線程安全的,但JDK 1.8通過一些機制(如synchronized關鍵字、CAS操作等)提高了其在多線程環境下的性能。然而,對于需要線程安全的場景,仍然建議使用ConcurrentHashMap。
代碼講解
下面是一些簡化的源碼片段和注釋,用于說明JDK 1.7和JDK 1.8中HashMap
的關鍵部分。
JDK 1.7 HashMap 簡化源碼
// JDK 1.7 HashMap 的簡化版本
public class HashMap<K, V> extends AbstractMap<K, V> implements Map<K, V>, Cloneable, Serializable {// 省略了大部分成員變量和方法...// 存儲元素的數組transient Entry<K, V>[] table;// Entry 是 HashMap 的一個內部類,表示一個鍵值對static class Entry<K, V> implements Map.Entry<K, V> {final K key;V value;Entry<K, V> next;// 省略了構造方法和其它方法...}// 省略了其它方法...// put 方法(簡化版)public V put(K key, V value) {// 對 key 進行哈希運算,定位到數組中的某個位置int hash = hash(key);int i = indexFor(hash, table.length);// 遍歷鏈表,查看是否已存在相同的 keyfor (Entry<K, V> e = table[i]; e != null; e = e.next) {if (e.hash == hash && (e.key.equals(key) || key.equals(e.key))) {V oldValue = e.value;e.value = value;return oldValue;}}// 如果不存在,則添加新的 Entry 到鏈表中addEntry(hash, key, value, i);return null;}// 添加新的 Entry 到數組中(鏈表形式)void addEntry(int hash, K key, V value, int bucketIndex) {Entry<K, V> e = table[bucketIndex];table[bucketIndex] = new Entry<>(hash, key, value, e);// 如果數組中的元素數量超過閾值,則進行擴容if (size++ > threshold)resize(2 * table.length);}// 省略了其它方法...
}
JDK 1.8 HashMap 簡化源碼
// JDK 1.8 HashMap 的簡化版本
public class HashMap<K, V> extends AbstractMap<K, V> implements Map<K, V>, Cloneable, Serializable {// 省略了大部分成員變量和方法...// 存儲元素的數組(在 JDK 1.8 中,這個數組可能存儲 Node 或 TreeNode)transient Node<K, V>[] table;// Node 是 JDK 1.8 中新引入的一個內部類,用于替代 Entrystatic class Node<K, V> implements Map.Entry<K, V> {final int hash;final K key;V value;Node<K, V> next;// 省略了構造方法和其它方法...}// 當鏈表長度超過8時,會轉換為紅黑樹,TreeNode 是紅黑樹的一個節點static final class TreeNode<K, V> extends LinkedHashMap.Entry<K, V> {TreeNode<K, V> parent;TreeNode<K, V> left;TreeNode<K, V> right;TreeNode<K, V> prev;// 省略了紅黑樹相關的其它方法和成員變量...}// 省略了其它方法...// put 方法(簡化版)public V put(K key, V value) {return putVal(hash(key), key, value, false, true);}// 實際的 put 邏輯(簡化版)final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {Node<K, V>[] tab;Node<K, V> p;int n, i;// 如果數組為空,則進行初始化if ((tab = table) == null || (n = tab.length) == 0)n = (tab = resize()).length;// 定位到數組中的某個位置,如果為空則直接插入新的 Nodeif ((p = tab[i = (n - 1) & hash]) == null)tab[i] = newNode(hash, key, value, null);else {// 如果不為空,則可能是鏈表或紅黑樹Node<K, V> e;K k;// 省略了部分邏輯,包括樹化處理和鏈表遍歷...// 如果找到了相同的 key,則更新 value 并返回舊值if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) {V oldValue = e.value;e.value = value;return oldValue;}// 如果沒找到相同的 key,則將新的 Node 插入到鏈表的末尾(或樹中)// 省略了具體的插入邏輯...}// 省略了其它邏輯,包括擴容和計數更新...return null;}// 省略了其它方法...
}
注釋說明
- 在JDK 1.7中,
HashMap
使用Entry
內部類來表示鍵值對,而在JDK 1.8中,引入了Node
內部類來替代Entry
。 - JDK 1.8中新增了
TreeNode
內部類,用于在鏈表長度超過8時將鏈表轉換為紅黑樹,以提高性能。 - JDK 1.8中的
put
方法邏輯更加復雜,因為它需要處理鏈表和紅黑樹兩種情況。 - 擴容機制在JDK 1.8中也有所改進,使得擴容過程更加平滑。