JDK1.8 之前
在 JDK1.8 之前,HashMap
的底层实现是 数组和链表 结合在一起使用的,也就是 链表散列。HashMap
通过 key 的 hashCode
经过扰动函数处理后得到 hash 值,然后通过 (n - 1) & hash
判断当前元素存放的位置(这里的 n 指的是数组的长度)。如果当前位置已经存在元素,就判断该元素与要存入的元素的 hash 值以及 key 是否相同;如果相同的话,直接覆盖;如果不相同就通过拉链法解决冲突。
扰动函数
所谓扰动函数是 HashMap
的 hash
方法。使用扰动函数是为了防止一些实现比较差的 hashCode()
方法,使用扰动函数之后可以减少碰撞。
JDK 1.8 的 hash 方法源码:
java
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
对比一下 JDK1.7 的 HashMap 的 hash 方法源码:
java
static int hash(int h) {
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
JDK1.8 的 hash 方法相比于 JDK1.7 更加简化,但原理不变。JDK1.7 的 hash 方法扰动次数更多(4次),性能稍差一些。
链表散列
所谓 “拉链法” 就是将链表和数组相结合,即创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中。
java
public class HashMap<K,V> {
// 默认的初始容量为16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
// 负载因子为0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 存储数据的数组
transient Node<K,V>[] table;
// 链表节点
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;
}
// 省略其他方法...
}
}
JDK1.8 之后
相比于之前的版本,JDK1.8 之后在解决哈希冲突时有了较大的变化。当链表长度大于阈值(默认为 8)时,将链表转化为红黑树,以减少搜索时间。链表转红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树。
链表转红黑树
我们来结合源码分析一下 HashMap
链表到红黑树的转换。
putVal
方法中执行链表转红黑树的判断逻辑
链表的长度大于 8 的时候,就执行 treeifyBin
(转换红黑树)的逻辑。
java
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;
// 计算索引
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1)
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) {
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
treeifyBin
方法中判断是否真的转换为红黑树
java
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);
}
}
将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树。
总结
通过对 HashMap 的源码解读,可以看出 JDK1.8 之前和之后在解决哈希冲突时的不同策略。JDK1.8 之前采用的是简单的链表散列法,而 JDK1.8 之后为了提高性能,引入了红黑树,通过链表和红黑树的动态转换来优化性能。这些改进使得 HashMap 在大多数情况下都能保持较高的性能。