从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中也有所改进,使得扩容过程更加平滑。