题目:JDK1.8 中 HashMap 有哪些改变?
难度:medium
标签:java/collections, general/knowledge
评分:⭐⭐⭐☆☆(3/5)
JDK 1.8 中 HashMap 的改动,首先核心是处理了并发下它的死循环问题,把原本的头插法改成了尾插法。头插法原本头插法出现,可能会出现,在断头的过程中,其他线程的插入,导致出现头指尾、尾指头,整个变成了死循环,产生导致程序崩溃的问题。然后其次在 JDK 1.8 中引入了,当单条链哈希冲突到 8 个以上的时候,会进行红黑树扩容。引入了红黑树这个架构数据结构,替换了原本的链表,提升了对于数据检索的效率。然后再然后是关于扩容方式,原本是通过扩容之后,每个元素进行哈希重新分配,修改后使用位运算,当新旧值对比位运算是的值是位运算是 0 的时候就不用移,如果是 1 的话就进行迁移到新的位置,扩容后的新的位置,只用移一部分元素,不用全部元素再进行迁移。
优点:
e.hash & oldCap 位运算的核心逻辑(0 留原位、非 0 迁新位置)不足:
hash(key) = key.hashCode() ^ (key.hashCode() >>> 16)——高位异或到低位参与运算,减少碰撞Entry 改名为 Node,新增 TreeNode(红黑树节点,双向链表+树指针),继承 LinkedHashMap.Entry 保留双向链表语义补充(JDK8 HashMap 完整改动清单):
TreeNode(继承 LinkedHashMap.Entry)(h ^ (h >>> 16)) 高位参与运算e.hash & oldCap 判断原位或 +oldCap,避免重新 hashcompute、computeIfAbsent、computeIfPresent、merge核心考点定位:验证对 HashMap JDK7 → JDK8 演进的理解,尤其每个改动背后的性能与正确性权衡。
知识点拆解:
hash(key) = (h = key.hashCode()) ^ (h >>> 16),把高 16 位异或到低 16 位。目的:让高位也参与桶下标计算,减少低位分布均匀时的高位碰撞。e.hash & oldCap 判断元素留原位还是搬到 index+oldCap。原理:容量是 2 的幂,扩容后新旧 index 只差第 n 位。避免重新 hash,节省约一半运算。按「底层原理 → 设计权衡 → 工程实践」递进排列。
考察方向:
ConcurrentHashMap JDK8 的类似改动(也引入红黑树、桶级 synchronized)延伸递进面试题:
computeIfAbsent 相比手写 if-null-then-put 有什么优势?ConcurrentHashMap 上能保证原子性(同桶内计算+插入);配合函数式编程减少样板代码。但注意 computeIfAbsent 里若调用会导致死锁的操作(如又调回同 Map 的方法)会出问题。面试官您好,JDK8 对 HashMap 的改动主要集中在四个层面。
第一是数据结构,从"数组+链表"扩展为"数组+链表+红黑树"。当某个桶的链表长度超过 8 且整个 HashMap 的容量超过 64 时,链表会转换成红黑树,把该桶内的查找从 O(n) 降到 O(log n)。阈值 8 来自泊松分布,自然概率下链长到 8 几乎为零,超过说明是哈希冲突而非分布问题。扩容时如果树桶 size 小于 6 会退化回链表。
第二是 hash 算法的优化。JDK8 的 hash 函数是 hash(key) = key.hashCode() ^ (key.hashCode() >>> 16),把 hashCode 的高 16 位异或到低 16 位参与运算,让高位也参与桶下标的计算,减少碰撞。
第三是头插法改尾插法。JDK7 用头插法扩容时,两个线程并发操作会形成环形链表,get 命中该桶 CPU 会 100%。JDK8 用尾插法保留了插入顺序,避免这个问题。要注意这只是修复了特定的并发 bug,HashMap 本身仍然不是线程安全的,size 计数、并发 put 等都有问题,并发场景还是要用 ConcurrentHashMap。
第四是扩容的效率优化。JDK7 扩容时对每个元素重新计算 hash 定位新桶;JDK8 用 e.hash & oldCap 判断——如果结果非 0 说明元素 hash 的第 n 位是 1,新位置是 index + oldCap;如果为 0 说明第 n 位是 0,元素留在原位。原理是容量是 2 的幂,新旧 index 只在第 n 位有差别,一个 bit 判断就能决定迁移方向,省去了重新 hash 的开销。
此外还引入了 TreeNode 类(继承 LinkedHashMap.Entry,同时含树指针和双向链表指针),以及一批新的函数式 API:compute、computeIfAbsent、computeIfPresent、merge,配合 lambda 表达式的场景。总的来说 JDK8 从数据结构、hash 算法、并发正确性、扩容效率四个维度做了系统性优化,性能提升了大约 30%-50%。