上一篇:Queue · 下一篇:ConcurrentHashMap
核心原理
Map 家族:键值映射
Map 与 Collection 并列,保存键值对。常见实现:
HashMap:最常用,不保证顺序LinkedHashMap:保持插入顺序TreeMap:按键排序Hashtable:老的线程安全实现ConcurrentHashMap:并发场景 → 专篇
Map<String, Integer> scores = new HashMap<>();scores.put("Tom", 90);System.out.println(scores.get("Tom"));键不能重复;同一键再次 put 会覆盖旧值。
HashMap 和 Hashtable 的区别
| HashMap | Hashtable | |
|---|---|---|
| 线程安全 | 否 | 是(方法级同步) |
| 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.7 | JDK 1.8 |
|---|---|---|
| 结构 | 数组 + 链表 | 数组 + 链表 + 红黑树 |
| 插入 | 头插法 | 尾插法 |
| 扩容重分布 | 重新计算 hash | (e.hash & oldCap)==0 留原槽,否则 原索引 + oldCap |
| 树化 | 无 | 链表 > 8 且数组 ≥ 64 |
树化:数组太小时会先 resize 扩容,不是一到 8 就树化。
适合做 key 的类型:String、Integer 等不可变且已实现 equals/hashCode 的类型。自定义 key 必须同时重写两者。
HashMap 的 put 与扩容流程
table为空 →resize()(默认容量 16,负载因子 0.75)- 计算
hash,i = (n - 1) & hash - 桶空 → 放
Node - 桶非空:key 相同 → 覆盖;
TreeNode→ 树插入;否则链表尾插 - 链表长度达 8 → 可能
treeifyBin ++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、时代背景。