Wrayの知识库 Wrayの知识库
首页
  • Java 基础
  • Java 集合
  • Java 并发
  • Java IO
  • JVM
  • Spring Framework
  • Spring Boot
  • Spring Cloud
  • Spring Security
  • MySQL
  • Redis
  • 计算机基础
  • 操作系统原理
  • Linux
  • MacOS
  • Windows
  • 系统工程与研究专题
  • AI 基础
  • 大模型基础
  • Prompt 工程
  • RAG 检索增强生成
  • Agent 智能体
  • AI 应用开发
  • AI 工程化
  • AI 安全与治理
  • AI 面试与设计题
  • 纸质书
  • 电子书
  • 学习课程
疑难杂症
GitHub (opens new window)
首页
  • Java 基础
  • Java 集合
  • Java 并发
  • Java IO
  • JVM
  • Spring Framework
  • Spring Boot
  • Spring Cloud
  • Spring Security
  • MySQL
  • Redis
  • 计算机基础
  • 操作系统原理
  • Linux
  • MacOS
  • Windows
  • 系统工程与研究专题
  • AI 基础
  • 大模型基础
  • Prompt 工程
  • RAG 检索增强生成
  • Agent 智能体
  • AI 应用开发
  • AI 工程化
  • AI 安全与治理
  • AI 面试与设计题
  • 纸质书
  • 电子书
  • 学习课程
疑难杂症
GitHub (opens new window)
  • Java章节编写规范
  • Java基础

  • Java集合

    • Java集合概述
    • ArrayList
    • LinkedList
    • HashMap
      • 1. 核心特点
      • 2. 底层结构
      • 3. put 流程
      • 4. get 流程
      • 5. 哈希扰动与下标计算
      • 6. 容量、负载因子和阈值
      • 7. 为什么容量是 2 的幂
      • 8. 扩容机制
      • 9. 哈希冲突与树化
      • 10. equals 与 hashCode
      • 11. 可变对象作为 key 的风险
      • 12. null key 与 null value
      • 13. 常用增强方法
        • 13.1 putIfAbsent
        • 13.2 computeIfAbsent
        • 13.3 merge
      • 14. 遍历方式
      • 15. 线程安全
      • 16. LinkedHashMap 与 TreeMap 对比
      • Tips 快问快答
    • LinkedHashMap
    • HashSet
    • TreeMap
    • Queue&Deque
    • 迭代器与遍历机制
    • Collections工具类
    • 集合排序与比较器
    • 集合选型与常见问题
  • Java并发

  • Java IO

  • JVM

目录

HashMap

HashMap 是 Java 中最常用的键值对集合。它基于哈希表实现,通过 key 快速定位 value,平均情况下 put、get、remove 的时间复杂度接近 O(1)。

HashMap 是集合章节的重点,因为它串联了哈希算法、数组寻址、链表、红黑树、扩容、equals/hashCode、线程安全和很多经典面试题。

# 1. 核心特点

特点 说明
key-value 结构 根据 key 查找 value
key 唯一 相同 key 会覆盖旧 value
允许 null key 只能有一个 null key
允许 null value 可以有多个 null value
不保证顺序 遍历顺序不等于插入顺序
非线程安全 并发写入应使用 ConcurrentHashMap
平均查找快 哈希分布良好时接近 O(1)

示例:

Map<Long, String> userNameMap = new HashMap<>();
userNameMap.put(1L, "Tom");
userNameMap.put(2L, "Jerry");

System.out.println(userNameMap.get(1L)); // Tom

# 2. 底层结构

Java 8 之后,HashMap 的桶位结构可以是链表,也可以在冲突严重时转换成红黑树。

HashMap
┌──────────────────────────────────────┐
│ table: Node&lt;K,V>[]                   │
│ size                                 │
│ threshold                            │
│ loadFactor                           │
└──────────────────────────────────────┘

table
index 0  -> null
index 1  -> Node(k1,v1) -> Node(k2,v2)
index 2  -> null
index 3  -> TreeNode 红黑树
...

节点保存:

Node
├─ hash
├─ key
├─ value
└─ next

核心思想:

key
 │ hashCode
 ▼
hash 扰动
 │
 ▼
数组下标
 │
 ├─ 桶为空:直接放入
 └─ 桶非空:比较 key,追加链表或插入红黑树

# 3. put 流程

map.put(key, value);

简化流程:

计算 key 的 hash
  │
  ▼
table 是否初始化
  │
  ├─ 否:初始化数组
  └─ 是:继续
  │
  ▼
通过 (n - 1) &amp; hash 定位桶
  │
  ├─ 桶为空:新建节点
  │
  └─ 桶非空:
       ├─ key 相等:覆盖 value
       ├─ 桶是链表:遍历链表
       └─ 桶是红黑树:树插入
  │
  ▼
size + 1
  │
  ▼
超过 threshold 则扩容

key 相等判断:

p.hash == hash && (p.key == key || key.equals(p.key))

先比较 hash,再比较引用或 equals。

# 4. get 流程

map.get(key);

简化流程:

计算 hash
  │
  ▼
定位桶 index
  │
  ├─ 桶为空:返回 null
  ├─ 第一个节点 key 相等:返回 value
  ├─ 红黑树:按树查找
  └─ 链表:逐个比较 key

平均 O(1) 的前提是 hash 分布良好,冲突较少。

# 5. 哈希扰动与下标计算

HashMap 的容量通常保持为 2 的幂,下标计算可以用位运算:

index = (capacity - 1) &amp; hash

例如容量 16:

capacity - 1 = 15 = 0000 1111
hash         =      xxxx yyyy
index        =      0000 yyyy

如果只用低位,某些 key 的高位差异无法参与下标计算。因此实现会做 hash 扰动,让高位信息也影响低位。

原始 hashCode
   │
   ▼
高 16 位与低 16 位混合
   │
   ▼
用于桶定位的 hash

这不是为了加密,而是为了让元素分布更均匀。

# 6. 容量、负载因子和阈值

核心参数:

参数 含义
capacity table 数组长度
loadFactor 负载因子,默认 0.75
threshold 扩容阈值,通常是 capacity * loadFactor
size 当前键值对数量

默认负载因子 0.75 是时间和空间的折中:

  • 太小:空间浪费,扩容更频繁。
  • 太大:冲突增加,查询变慢。

当:

size > threshold

触发扩容。

# 7. 为什么容量是 2 的幂

容量为 2 的幂时,下标计算可以用位运算替代取模:

hash % capacity

可以优化成:

hash &amp; (capacity - 1)

同时扩容时,节点位置迁移也更高效。

扩容为两倍后,旧节点的新位置只有两种:

原位置 index
或
index + oldCapacity

判断依据是:

hash &amp; oldCapacity

示意:

oldCap = 16
newCap = 32

旧桶 i 中节点:
  ├─ hash &amp; 16 == 0 -> 仍在 i
  └─ hash &amp; 16 != 0 -> 移到 i + 16

# 8. 扩容机制

扩容不是简单“数组变大”,而是重新分布桶位。

旧 table
index 1 -> A -> B -> C

扩容后
new index 1  -> A -> C
new index 17 -> B

扩容成本:

  • 创建新数组。
  • 遍历旧数组。
  • 迁移链表或树节点。
  • 更新阈值。

如果能预估元素数量,应指定初始容量,减少扩容:

Map<Long, User> map = new HashMap<>(expectedCapacity);

但构造参数是容量,不是元素个数。若预计放入 n 个元素,可以估算:

initialCapacity ≈ n / loadFactor + 1

例如预计 1000 个元素:

Map<Long, User> map = new HashMap<>(1340);

JDK 会把容量调整到合适的 2 的幂。

# 9. 哈希冲突与树化

多个 key 定位到同一个桶,就是哈希冲突。

table[5]
  │
  ▼
Node A -> Node B -> Node C -> ...

冲突过多时,链表查询会从 O(1) 退化为 O(n)。

Java 8 之后,当链表长度达到一定阈值,且数组容量足够大时,会树化为红黑树。

链表
A -> B -> C -> D -> E -> F -> G -> H

树化后
       D
     /   \
    B     F
   / \   / \
  A  C  E  H
          /
         G

常见阈值:

参数 常见值 含义
TREEIFY_THRESHOLD 8 链表达到该长度可能树化
UNTREEIFY_THRESHOLD 6 树节点减少到该长度可能退化
MIN_TREEIFY_CAPACITY 64 table 至少达到该容量才树化

如果 table 太小,优先扩容而不是树化,因为扩容可能直接减少冲突。

# 10. equals 与 hashCode

HashMap 的 key 必须满足 equals/hashCode 契约。

equals 相等 -> hashCode 必须相等
hashCode 相等 -> equals 不一定相等

错误示例:

public class UserKey {
    private Long id;

    @Override
    public boolean equals(Object obj) {
        return obj instanceof UserKey other && Objects.equals(id, other.id);
    }

    // 忘记重写 hashCode
}

后果:

Map<UserKey, String> map = new HashMap<>();
map.put(new UserKey(1L), "Tom");

System.out.println(map.get(new UserKey(1L))); // 可能是 null

因为两个逻辑相等的 key 可能定位到不同桶。

正确写法:

@Override
public int hashCode() {
    return Objects.hash(id);
}

# 11. 可变对象作为 key 的风险

不要使用会变化的字段参与 key 的 hashCode 和 equals。

UserKey key = new UserKey(1L);
map.put(key, "Tom");

key.setId(2L);
System.out.println(map.get(key)); // 可能取不到

过程:

put 时:id=1 -> 桶 A
修改后:id=2 -> 查找桶 B
原节点还在桶 A,get 失败

适合作为 key 的对象应尽量不可变,例如 String、Long、枚举、自定义不可变值对象。

# 12. null key 与 null value

HashMap 允许一个 null key:

map.put(null, "default");

也允许多个 null value:

map.put("A", null);
map.put("B", null);

因此:

map.get("A") == null

可能表示:

  • key 不存在。
  • key 存在,但 value 就是 null。

需要区分时:

if (map.containsKey("A")) {
    // key 存在
}

# 13. 常用增强方法

# 13.1 putIfAbsent

map.putIfAbsent(key, value);

key 不存在或当前 value 为 null 时放入。

# 13.2 computeIfAbsent

List<Order> orders = orderMap.computeIfAbsent(userId, k -> new ArrayList<>());
orders.add(order);

常用于分组收集。

注意:不要在计算函数里对同一个 Map 做复杂结构修改,容易引入可读性和递归问题。

# 13.3 merge

统计次数:

Map<String, Integer> countMap = new HashMap<>();
countMap.merge("Java", 1, Integer::sum);

等价于:

如果 key 不存在 -> 放入 1
如果 key 存在   -> oldValue + 1

# 14. 遍历方式

推荐遍历 entry:

for (Map.Entry<Long, User> entry : userMap.entrySet()) {
    Long id = entry.getKey();
    User user = entry.getValue();
}

如果只需要 key:

for (Long id : userMap.keySet()) {
}

如果只需要 value:

for (User user : userMap.values()) {
}

不推荐:

for (Long id : userMap.keySet()) {
    User user = userMap.get(id);
}

这会二次查找。

# 15. 线程安全

HashMap 非线程安全,多线程并发写入会出现数据竞争。

错误用法:

Map<String, String> map = new HashMap<>();
// 多个线程同时 put

并发场景选择:

场景 推荐
并发读写 Map ConcurrentHashMap
初始化后只读 构建完成后安全发布,或使用不可变 Map
简单同步 Collections.synchronizedMap
顺序 + 并发 通常需要额外设计,不要直接套普通 Map

HashMap 在并发下的问题不是“偶尔慢”,而是语义不安全。

# 16. LinkedHashMap 与 TreeMap 对比

实现 顺序 查找复杂度 适合场景
HashMap 不保证 平均 O(1) 普通映射
LinkedHashMap 插入或访问顺序 平均 O(1) 有序遍历、LRU
TreeMap key 排序 O(log n) 排序、范围查询

如果只是希望遍历顺序稳定,优先 LinkedHashMap;如果需要按 key 比较大小、查区间,选择 TreeMap。

# Tips 快问快答

Q:HashMap 为什么快? A:通过 key 的 hash 快速定位数组桶,冲突少时无需遍历太多元素。

Q:HashMap 一定是 O(1) 吗? A:不是。平均接近 O(1),冲突严重时会退化,树化后相关桶操作约 O(log n)。

Q:为什么容量要是 2 的幂? A:可以用 (n - 1) & hash 快速定位桶,并简化扩容迁移。

Q:负载因子为什么默认 0.75? A:这是空间利用和冲突概率之间的折中。

Q:链表长度到 8 一定树化吗? A:不一定,还要看 table 容量是否至少达到树化最小容量,容量太小时优先扩容。

Q:HashMap 允许 null key 吗? A:允许一个 null key,也允许多个 null value。

Q:get(key) == null 能说明 key 不存在吗? A:不能,value 可能就是 null。需要用 containsKey 区分。

Q:自定义对象做 key 要注意什么? A:正确重写 equals 和 hashCode,并避免参与计算的字段变化。

Q:HashMap 线程安全吗? A:不安全,并发读写应使用 ConcurrentHashMap 或外部同步。

Q:为什么遍历 Map 推荐 entrySet? A:可以一次拿到 key 和 value,避免先遍历 key 再二次查找 value。

上次更新: 2026/06/24, 16:37:44
LinkedList
LinkedHashMap

← LinkedList LinkedHashMap→

Copyright © 2023-2026 Wray | 鄂ICP备2024050235号-1
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式