HashMap 底层原理(面试精简版)

0 阅读1分钟

HashMap 底层原理(面试精简版)

1. 底层数据结构

  • JDK 1.7:数组 + 链表
  • JDK 1.8+:数组 + 链表 + 红黑树
    • 链表长度 > 8 且数组长度 ≥ 64 时,链表转为红黑树
    • 红黑树节点数 < 6 时,退化为链表

2. 核心常量

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 默认初始容量 16
static final float DEFAULT_LOAD_FACTOR = 0.75f;     // 默认负载因子
static final int TREEIFY_THRESHOLD = 8;             // 树化阈值
static final int UNTREEIFY_THRESHOLD = 6;           // 链化阈值
static final int MIN_TREEIFY_CAPACITY = 64;         // 最小树化容量

3. Put 流程(JDK 1.8)

  1. 计算 hash(key)(key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16)(扰动函数,高低位异或)
  2. 若数组为空,触发 resize() 初始化
  3. 计算索引:(n - 1) & hash,若该位置为空,直接插入
  4. 若不为空:
    • 检查 key 是否相等,相等则覆盖
    • 若为红黑树节点,调用 putTreeVal
    • 若为链表,遍历到尾插(JDK 1.7 头插,1.8 改为尾插),同时计数,超过 8 则转红黑树
  5. 插入后判断 size > threshold,是则扩容

4. 扩容机制

  • 触发条件size >= thresholdthreshold = capacity * loadFactor
  • 扩容大小:新容量 = 旧容量 << 1(翻倍)
  • 重哈希:节点重新计算索引,JDK 1.8 优化:
    • 节点在原位置或 原位置 + 旧容量,通过 (e.hash & oldCap) == 0 判断
    • 无需重新计算 hash,减少开销

5. 线程安全性

  • HashMap 非线程安全
  • 并发场景下可能产生:
    • JDK 1.7 头插法导致死循环(多线程扩容时)
    • 数据丢失、size 不准确
  • 替代方案:ConcurrentHashMapCollections.synchronizedMap

6. 关键知识点

问题

答案

为什么容量是 2 的幂

方便取模运算 (n - 1) & hash,保证均匀分布

负载因子为什么 0.75

时间与空间平衡的折中

为什么引入红黑树

防止链表过长导致查询 O(n) 退化为 O(log n)

null key 存在位置

固定存放在数组 0 号桶

7. JDK 1.7 vs 1.8 区别

对比项

JDK 1.7

JDK 1.8

数据结构

数组 + 链表

数组 + 链表 + 红黑树

插入方式

头插法

尾插法

扩容重哈希

重新计算每个 hash

优化为原位置或原位置+旧容量

扰动函数

多次异或

一次异或(高16位与低16位异或)

面试常问:“HashMap 的 get 流程?” → 计算 hash → 定位桶 → 遍历链表/红黑树,用 equals() 比较 key 返回 value。