那天面试一个号称5年经验的候选人,我随口问了句:"HashMap底层实现了解吗?"他秒答:"数组+链表。"我心想,这答案放在2014年还算标准,可现在都2024年了…
于是我继续追问:"JDK1.8之后有什么变化?"这时候他开始支支吾吾,最后憋出一句:"好像加了什么树?"
哈希冲突:不是所有的碰撞都是意外
HashMap的核心就是通过hash值快速定位数组下标,理想情况下时间复杂度O(1)。但现实很骨感,不同的key算出相同的hash值太正常了。
// 这就是经典的哈希冲突场景
Map<String, String> map = new HashMap<>();
map.put("Aa", "value1"); // "Aa".hashCode() = 2112
map.put("BB", "value2"); // "BB".hashCode() = 2112
你有没有想过,为什么HashMap的默认容量是16,而且必须是2的幂次?
这背后的巧思在于位运算优化。hash & (n-1)
等价于 hash % n
,但位运算的性能比取模快得多。当容量是2的幂次时,n-1
的二进制全是1,保证了hash值的均匀分布。
链表到红黑树:性能优化的华丽转身
JDK1.8之前,冲突元素通过链表存储。想象一下极端场景:如果有恶意攻击者故意构造大量hash冲突的数据,单个链表可能变成几千个节点。这时候查找一个元素就变成了O(n)的线性查找,HashMap瞬间退化成了"链表Map"。
// JDK1.8的阈值设计很有讲究
static final int TREEIFY_THRESHOLD = ; // 链表转红黑树阈值
static final int UNTREEIFY_THRESHOLD = ; // 红黑树转链表阈值
static final int MIN_TREEIFY_CAPACITY = ; // 最小树化容量
为啥是8这个神奇数字?源码注释里写得很清楚:根据泊松分布的统计,在负载因子0.75的情况下,单个桶内元素个数达到8的概率约为0.00000006,基本不可能。
但你注意到没有,转回链表的阈值是6而不是8?这是个很巧妙的滞后策略,避免链表和红黑树之间频繁转换。我之前就踩过这个坑,写了个压测程序反复增删,结果发现性能抖动特别厉害,查了半天才发现是频繁树化导致的。
红黑树:平衡的艺术
红黑树本质上是一颗自平衡的二叉搜索树,保证了最坏情况下的查找时间复杂度是O(log n)。但它比AVL树更适合HashMap这种场景,因为红黑树的插入删除操作旋转次数更少。
// TreeNode继承了LinkedHashMap.Entry,巧妙地保持了插入顺序
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;
boolean red;
}
这里有个很多人忽略的细节:TreeNode还保持了双向链表的结构(prev字段)。为啥要这么设计?因为在树化过程中需要保持原有的插入顺序,这样在退化回链表时才能无缝切换。
扩容机制:成长的烦恼
HashMap的扩容是个重头戏,每次扩容都是2倍增长。但扩容时的rehash过程很有意思:
// JDK1.8优化了扩容时的节点分布算法
if ((e.hash & oldCap) == ) {
// 保持在原来的位置
loTail.next = e;
} else {
// 移动到 原位置+oldCap 的位置
hiTail.next = e;
}
这个优化很巧妙,利用了容量是2的幂次的特性。原来需要重新计算每个元素的位置,现在只需要看多出来的那一位是0还是1,元素要么保持原位,要么移动到"原位置+旧容量"的位置。
负载因子0.75:权衡的智慧
很多人背书式地记住了0.75这个数字,但很少有人深入思考过为什么。
负载因子越小,哈希冲突越少但空间浪费越大;负载因子越大,空间利用率高但冲突概率增加。0.75是个经过大量实验验证的平衡点,在时间和空间复杂度之间找到了最佳权衡。
线程安全:并发下的灾难
最后聊个生产中的血泪教训。HashMap在多线程环境下不仅仅是数据不一致的问题,JDK1.7及之前版本在并发扩容时还可能形成链表环,导致CPU 100%的死循环。
我记得有次生产故障,服务器CPU突然飙升,最后排查发现是HashMap并发扩容导致的。从那以后,多线程场景我都老老实实用ConcurrentHashMap。
看似简单的HashMap,背后藏着这么多精妙的设计。下次面试官再问你HashMap,你是不是已经有了P8级别的底气?
想想看,还有哪些经典数据结构的设计让你印象深刻?