HashMap 原理

上一篇:Queue · 下一篇:ConcurrentHashMap

核心原理#

Map 家族:键值映射#

MapCollection 并列,保存键值对。常见实现:

  • HashMap:最常用,不保证顺序
  • LinkedHashMap:保持插入顺序
  • TreeMap:按键排序
  • Hashtable:老的线程安全实现
  • ConcurrentHashMap:并发场景 → 专篇
Map<String, Integer> scores = new HashMap<>();
scores.put("Tom", 90);
System.out.println(scores.get("Tom"));

键不能重复;同一键再次 put覆盖旧值。

HashMap 和 Hashtable 的区别#

HashMapHashtable
线程安全是(方法级同步)
null允许一个 null 键、多个 null 值不允许
推荐度单线程/外部控并发时首选已过时
Map<String, Integer> scores = new HashMap<>();
scores.put(null, 0);
Map<String, Integer> table = new Hashtable<>();
// table.put(null, 1); // NullPointerException

并发读写优先 ConcurrentHashMap,而不是 Hashtable

HashMap 原理:结构、哈希与版本差异#

数组 + 链表(JDK 8 起链表过长会树化为红黑树),链地址法解决冲突。

hash 扰动(JDK 8):(h = key.hashCode()) ^ (h >>> 16)。下标:index = (table.length - 1) & hash,要求 table 长度为 2 的幂

维度JDK 1.7JDK 1.8
结构数组 + 链表数组 + 链表 + 红黑树
插入头插法尾插法
扩容重分布重新计算 hash(e.hash & oldCap)==0 留原槽,否则 原索引 + oldCap
树化链表 > 8 且数组 ≥ 64

树化:数组太小时会先 resize 扩容,不是一到 8 就树化。

适合做 key 的类型StringInteger 等不可变且已实现 equals/hashCode 的类型。自定义 key 必须同时重写两者。

HashMap 的 put 与扩容流程#

  1. table 为空 → resize()(默认容量 16,负载因子 0.75)
  2. 计算 hashi = (n - 1) & hash
  3. 桶空 → 放 Node
  4. 桶非空:key 相同 → 覆盖;TreeNode → 树插入;否则链表尾插
  5. 链表长度达 8 → 可能 treeifyBin
  6. ++size > threshold → 扩容为 2 倍

扩容(JDK 8):节点留在原索引或移到 原索引 + oldCap,由 (e.hash & oldCap) == 0 判断。

map.put("Tom", 90);
map.put("Tom", 95); // 覆盖 value

常见陷阱#

Map.get 可能返回 null#

键不存在时 get 返回 null,拆箱成 int 前注意 NPE。

误以为 HashMap 线程安全#

多线程读写同一 HashMap 可能数据不一致;用 ConcurrentHashMap 或加锁。

自定义 key 未重写 equals/hashCode#

逻辑相等的键可能查不到或无法覆盖。

面试速记#

  • HashMap 结构:数组 + 链表 + 红黑树(JDK 8)。
  • 容量 2 的幂(n-1) & hash 等价 hash % n,更快。
  • HashMap vs Hashtable:线程安全、null、时代背景。

集合面试 52 道

文章目录

文章目录