Java中的HashMap是一种非常常用的数据结构,它以键-值对的形式存储数据,并能快速地进行数据的查找、插入和删除操作。在JDK1.8以后,HashMap的内部结构发生了一些重要的变化,其中最显著的变化是引入了红黑树来处理哈希冲突,以提高查询性能。本文将详细描述这些变化,并提供相关的源码片段进行解析。
在了解JDK1.8以后HashMap的变化之前,HashMap采用数组+链表的数据结构,其中数组是HashMap的主体,每个数组元素都是一个桶(bucket),而链表则主要用来解决哈希冲突。当发生哈希冲突时,具有相同哈希值的元素会存储在同一个链表中。
HashMap的基本结构可以分点描述如下:
综上所述,HashMap通过结合数组、链表和红黑树的数据结构,以及哈希算法和扩容机制,实现了高效的键值对存储和查找操作。
在JDK 8及以后的版本中,HashMap在处理哈希冲突时引入了红黑树的数据结构。这种改变主要是为了优化在哈希冲突严重时HashMap的性能。
1. 哈希冲突与链表
在早期的HashMap实现中,当发生哈希冲突时,即将不同的键计算出的哈希值相同时,这些键值对会以链表的形式存储在同一个桶(bucket)中。然而,当哈希冲突变得非常严重时,链表会变得很长,导致在查找、插入和删除操作时的性能下降。具体来说,链表的查找操作需要遍历整个链表,时间复杂度为O(n),其中n是链表的长度。
2. 引入红黑树
为了解决这个问题,JDK 8对HashMap进行了改进。当链表长度达到一定阈值(默认为8)时,链表会转换为红黑树。红黑树是一种自平衡的二叉查找树,它的查找、插入和删除操作的时间复杂度为O(log n),其中n是树的节点数。与链表相比,红黑树在性能上更有优势。
3. 红黑树的优势
红黑树作为一种自平衡的二叉查找树,具有以下优势:
4. 阈值的选择
在JDK 8中,链表转换为红黑树的阈值默认为8。这个值是通过权衡性能和空间开销来选择的。当链表长度超过8时,转换为红黑树可以提高查找性能;而当链表长度较短时,由于红黑树的维护成本相对较高,因此保持链表结构更为合适。
在 JDK 8 的 HashMap
实现中,引入了扰动函数(perturbation function)来优化哈希算法。扰动函数的主要目的是增加哈希值的随机性,使得键值对能够更均匀地分布在哈希表中,从而减少哈希冲突和提高查询效率。
具体来说,HashMap
中的扰动函数实现如下:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
这里,key.hashCode()
是调用键对象的 hashCode()
方法来获取其哈希值。然后,这个哈希值会经过一个额外的步骤:与其自身的高 16 位进行异或(XOR)运算。异或运算是一种位运算,对应位上的数字相同则结果为 0,不同则结果为 1。
HashMap
实现中,由于哈希表的大小通常是 2 的幂次方,因此只使用了哈希值的低位来进行索引计算(通过位运算 (n - 1) & hash
)。这样做忽略了哈希值的高位信息。通过扰动函数,HashMap
能够更好地利用整个哈希值的信息。综上所述,通过引入扰动函数,JDK 8 的 HashMap
实现了对哈希值的进一步优化,使得键值对能够更均匀地分布在哈希表中,提高了查询效率和整体性能。
下面将通过源码片段来解析JDK1.8以后HashMap结构的变化。
在 JDK 8 的 HashMap
实现中,Node
类是一个静态内部类,用于存储哈希表中的键值对。每个 Node
对象都持有一个键、一个值、一个指向下一个节点的引用(用于解决哈希冲突)以及该节点的哈希值。
下面是 HashMap
中 Node
类的简化定义:
static class Node<K,V> implements Map.Entry<K,V> {
final int hash; // 哈希值
final K key; // 键
V value; // 值
Node<K,V> next; // 指向下一个节点的引用,用于链表或红黑树
// 构造函数,用于创建新的节点
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
// 实现 Map.Entry 接口的方法
public final K getKey() {
return key;
}
public final V getValue() {
return value;
}
public final String toString() {
return key + "=" + value;
}
// 设置新值并返回旧值
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
// 判断两个节点是否相等(基于键和值的 equals 方法)
public final boolean equals(Object o) {
// ... 省略了具体的实现细节
}
// 返回节点的哈希码(基于键的 hashCode 方法和值的 hashCode 方法)
public final int hashCode() {
// ... 省略了具体的实现细节
}
// ... 可能还有其他方法或字段,例如用于红黑树节点的 left 和 right 字段等
}
需要注意的是,上面的代码是一个简化的版本,并没有包含所有的方法和字段。在实际的 JDK 8 HashMap
源码中,Node
类还可能会包含其他用于红黑树操作的字段和方法,例如 left
和 right
指针等。但是,对于基本的哈希表操作来说,上面的定义已经足够说明问题了。
每个 Node
对象都通过 next
引用连接在一起,形成链表,用于解决哈希冲突。当哈希表中的某个索引位置上有多个键值对的哈希值相同时,这些键值对就会以链表的形式存储在该索引位置上。在 JDK 8 中,当链表长度超过一定阈值(默认为 8)时,链表会转换为红黑树,以进一步提高查询效率。但是,Node
类本身并不直接支持红黑树的操作,而是通过继承自 Node
的其他类(如 TreeNode
)来实现的。
在 JDK 8 的 HashMap
实现中,TreeNode
类是 Node
类的子类,用于实现红黑树结构以优化哈希表在高度冲突情况下的性能。当某个索引位置的链表长度超过一定阈值(默认为 8)并且哈希表的大小大于或等于 64 时,链表就会转换为红黑树,此时节点类型会从 Node
变为 TreeNode
。
TreeNode
类除了包含 Node
类中的所有字段外,还添加了用于维护红黑树结构的额外字段和方法。下面是 TreeNode
类的一个简化定义:
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
TreeNode<K,V> left; // 指向左子节点的引用
TreeNode<K,V> right; // 指向右子节点的引用
TreeNode<K,V> parent; // 指向父节点的引用
boolean red; // 颜色标志,用于红黑树的平衡操作
TreeNode(int hash, K key, V value, Node<K,V> next) {
super(hash, key, value, next);
}
// 返回当前节点是红色还是黑色
final boolean isRed() {
return red;
}
// ... 其他用于维护红黑树结构和性能的方法,如旋转、重新着色等
}
需要注意的是,上面的代码是一个简化的版本,并没有包含所有的方法和字段。在实际的 JDK 8 HashMap
源码中,TreeNode
类会包含更多的方法和字段,以支持红黑树的各种操作。
然而,有一个重要的更正需要指出:在 JDK 8 的实际 HashMap
实现中,并没有一个名为 TreeNode
的公共类。相反,红黑树节点是通过 Node
类本身实现的,Node
类中包含了一些额外的字段来支持红黑树的操作。这些字段包括 left
、right
和一个用于表示颜色的字段(但在实际的 JDK 代码中并没有直接命名为 red
,而是通过位运算在哈希值中存储颜色信息)。
这里是一个更接近 JDK 8 源码的 Node
类定义,其中包含了红黑树相关的字段:
static final class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
Node<K,V> left; // 仅在红黑树中使用
Node<K,V> right; // 仅在红黑树中使用
// ... 构造函数、getter、setter 和其他方法
// 用于判断节点是否是红色(实际上是通过哈希值的一个位来判断)
final boolean isRed() {
// 在实际的 JDK 代码中,这里会是一个位运算操作来检查颜色位
// 例如:return (hash & RED_FLAG) != 0;
// 但为了简化说明,这里我们假设有一个布尔字段来表示颜色
// 注意:实际的 JDK 实现中并没有这样的布尔字段
return false; // 示例代码,实际上需要根据具体的位运算来判断
}
}
在实际的实现中,HashMap
通过一系列复杂的位运算和条件判断来管理节点的颜色以及红黑树的平衡性。当链表转换为红黑树时,节点的类型仍然是 Node
,但会利用额外的字段来构建和维护红黑树结构。
putVal
是 HashMap 中一个核心的私有方法,用于处理插入或更新键值对的逻辑。这个方法在 put
、putAll
和其他一些内部方法中都会被调用。以下是 putVal
方法的详细描述,包括其参数、主要逻辑和关键步骤。
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict)
hash
:键的哈希码,通过 key.hashCode()
获得,并可能经过进一步的处理。key
:要插入或更新的键。value
:与键相关联的值。onlyIfAbsent
:一个布尔值,当为 true
时,如果映射中已经包含键的映射关系,则不执行任何操作。这对应于 putIfAbsent
方法的行为。evict
:一个布尔值,当为 true
时,如果映射已超出最大容量并且需要移除旧元素以容纳新元素,则执行移除操作。这对应于在插入新元素时可能需要进行的扩容和/或元素移除。检查是否需要扩容:如果当前数组为空或长度为0,则调用 resize
方法进行扩容。
计算索引:使用哈希码计算键在数组中的索引位置。
处理哈希冲突:
onlyIfAbsent
的值决定是否更新值。TREEIFY_THRESHOLD
(默认为8),则将链表转换为红黑树。UNTREEIFY_THRESHOLD
(默认为6),则将红黑树退化为链表。检查是否需要扩容:在插入新元素后,如果HashMap的大小超过了阈值(即容量乘以加载因子),则进行扩容。
返回插入或更新的旧值:如果键已存在,则 putVal
方法返回旧值;否则返回 null
。
putVal
方法是 HashMap 中实现高效插入和更新操作的核心。由于 HashMap 是非同步的,因此在多线程环境下使用 putVal
时需要注意线程安全问题。如果需要线程安全的HashMap,可以考虑使用 Collections.synchronizedMap(new HashMap<>())
或 ConcurrentHashMap
。
treeifyBin
是 HashMap
中用于将链表转换为红黑树的方法。这个方法在 HashMap
的某个桶(bucket)中的链表长度超过一定阈值(默认为8)时被调用,以提高后续查找、插入和删除操作的效率。
下面是 treeifyBin
方法的详细描述:
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;
do {
TreeNode<K,V> p = replacementTreeNode(e, null);
if (tl == null)
hd = p;
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}
本文系作者在时代Java发表,未经许可,不得转载。
如有侵权,请联系nowjava@qq.com删除。