【面试突击】JAVA基础知识-集合

36 阅读4分钟

HashMap 1.8 全面解析与面试速答


一、概览

  • 底层结构:数组 Node<K,V>[] table + 桶内链表(发生冲突时)+ 桶内红黑树(高冲突时)。
  • JDK 1.8 关键升级点
    • 链表尾插(保持顺序,降低扩容期间产生环的风险),
    • 引入红黑树优化高冲突桶的查找效率。

二、核心参数(必须记牢)

  • 默认初始容量:16
  • 最大容量:1 << 30
  • 默认负载因子:0.75f
  • 链表转红黑树阈值:8
  • 红黑树退化回链表阈值:6
  • 允许树化最小表容量:64

三、定位桶的奥秘:hash 扰动与索引计算

// 1. 取 hash 值
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

// 2. 桶索引计算
index = (n - 1) & hash;
  • 扰动函数h ^ (h >>> 16),用于混合高位,减少 hash 冲突。
  • 下标计算(n - 1) & hash,n 为 table 长度且恒为 2 的幂,效率极高。

四、put 流程梳理

  1. 扰动 hash+定位槽位:提升分布均匀度,保证高效存取。
  2. table 初始化:若为空,resize() 初始化(默认16)。
  3. 槽位空/非空:空则直接插入,否则遍历桶(链表/树),已存在则覆盖 value,否则尾插新节点。
  4. 树化判定:桶内链表长≥8且 table 长≥64,才树化。
  5. 扩容判定:当前元素数≥容量 × 负载因子时自动扩容(容量翻倍)。

五、get 流程总结

  • hash 扰动后,按索引定位桶。
  • 桶为树,则树查找;为链表,则 equals 比较。
  • 命中返回 value,未命中返回 null。

六、resize 扩容机制(1.8 优化)

  • 触发点:size > threshold
  • 步骤:
    1. 建立新表(容量双倍),阈值随之变化。
    2. 遍历旧桶,将节点依次拆为低/高位两组
      • e.hash & oldCap == 0:原索引位置;
      • e.hash & oldCap != 0:新索引 = 原索引 + oldCap。
    3. 保持链表相对顺序(尾插),避免成环。
    4. 树形桶可能分为两棵更小的树,或退化成链表。

优化要点:利用位运算完成节点拆分,迁移零额外 hash 计算,极高效率。


七、树化与退化规则

  • 树化:单桶链表节点数≥8且表长≥64才树化。
  • 退化:树化桶节点减至≤6时自动回退为链表,节约内存。

大表遇真冲突才树化,避免在小表时过早树化带来空间浪费。


八、为何容量必须为 2 的幂?

  • (n - 1) & hash 高效取模;
  • 保证 hash 低位能均匀分布;
  • 扩容节点无需重新 hash,直接通过 & oldCap 分组,决定留原位or偏移,迁移 O(1)。

九、复杂度 & 空间分析

  • 平均查找/插入:O(1) 摊还
  • 极端冲突:链表 O(k),树化后 O(log k)
  • 树节点空间消耗高于链表。

十、线程安全问题(高频面试)

  • 并发 put:同桶冲突可能数据被覆盖。
  • 并发 resize:迁移中间态有概率丢节点/读不一致(1.7 更严重,1.8 尾插有所缓解)。
  • 可见性:无同步,get 可能看到“未完全发布”的数据。
  • 结论:多线程下 HashMap 绝不安全,强烈推荐用 ConcurrentHashMap!

十一、常见误区纠正

  • 不是 put 时就树化,要满足“链表长≥8 && 表长≥64”。
  • (n-1)&hash 能高效取模,前提是 n 一定为 2 的幂
  • JDK1.8 并不线程安全,只是降低了头插成环概率。

十二、面试速答超浓缩(30s)

HashMap 1.8: 底层“数组+链表+红黑树”,索引靠 hash 扰动和位运算,容量始终 2 的幂。put 命中桶,若树用树,链表用尾插,链表≥8且表≥64才树化,否则先扩容。扩容搬迁靠位运算判分组,不重新 hash,并发下不安全,推荐用 ConcurrentHashMap。


ConcurrentHashMap 1.8 深度笔记与速答


一、诞生背景

  • HashMap:线程不安全,多线程 put 会丢数据/死循环。
  • Hashtable:整表一把锁,性能极差。

ConcurrentHashMap = 线程安全 + 高性能的 HashMap。


二、底层结构

  • 数组 + 链表 + 红黑树与 HashMap 1.8 相同。
  • 数组类型:Node<K,V>[] table,桶里冲突少用链表,多时用红黑树。

可简单认为:加了并发控制的 HashMap。


三、三大核心特性

1. get:无锁读取

  • hash 定位桶,链表 or 树方式查找。
  • 全过程不加锁,只用 volatile 保证可见性,多线程读极快

2. put:只锁当前桶,小锁极细粒度

  • 定位桶,若空用 CAS 插入,无锁。

  • 非空则对桶头节点 synchronized,只锁“一小块”

  • 对比:

    • Hashtable:锁整表,写任何地方都竞争。
    • CHM:写不同桶互不影响。

3. 扩容:多线程协作搬迁

  • 多线程拆分桶任务,一起搬数据。
  • 被迁移过的桶用 ForwardingNode 特殊节点标记,线程可判断并协作或转向新表。

四、JDK 1.7/1.8 差别快答

  • **1.7:**分段锁(Segment)+每段锁一把;
  • **1.8:**去掉 Segment,直接桶粒度锁,更灵活高效。

五、面试重点与小结

  • 不允许 null key/null value
  • get 无锁,多线程读极快。
  • put/remove 只锁单个桶,极小锁冲突。
  • size() 高并发场景下为近似值。
  • 迭代弱一致,不抛 ConcurrentModificationException。

六、面试超浓缩模板

ConcurrentHashMap 1.8:底层和 HashMap 1.8 类似,但做了并发优化。get 无锁,仅读 volatile。put 时如果桶空用 CAS 插入,桶非空只对桶头加 synchronized,不锁全表。扩容支持多线程,协同搬迁桶数据。多线程安全且高性能。