乐途乐途
主页
  • 计算机基础

    • TCP/IP
    • Linux
    • HTTP
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C
    • C++
    • Java SE
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • Maven 入门
    • Maven 进阶
    • MyBatis
    • Spring
    • Spring MVC
  • Java

    • Spring Boot
    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Security
    • Spring AI
    • Spring Batch
    • Kafka
    • Java 设计模式
  • 缓存

    • Redis
  • 搜索引擎

    • Elasticsearch
  • 分布式协调

    • ZooKeeper
联系
阿里云
主页
  • 计算机基础

    • TCP/IP
    • Linux
    • HTTP
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C
    • C++
    • Java SE
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • Maven 入门
    • Maven 进阶
    • MyBatis
    • Spring
    • Spring MVC
  • Java

    • Spring Boot
    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Security
    • Spring AI
    • Spring Batch
    • Kafka
    • Java 设计模式
  • 缓存

    • Redis
  • 搜索引擎

    • Elasticsearch
  • 分布式协调

    • ZooKeeper
联系
阿里云
  • 学习路径
  • 第1章 Java概述与环境搭建

    • Java概述与环境搭建
    • Java语言概述
    • 解释型语言与编译型语言对比
    • JDK安装与配置
    • JDK、JRE、JVM 详解
    • HelloWorld程序详解
    • IDE 介绍
  • 第2章 标识符与基本数据类型

    • 章节导读
    • 变量概述
    • 常量概述
    • 基本类型与包装类
    • 字节型 byte
    • 短整型 short
    • 整型 int
    • 长整型 long
    • 单精度浮点型 float
    • 双精度浮点型 double
    • 字符型 char
    • 布尔型 boolean
    • 类型转换
  • 第3章 运算符与表达式

    • 章节导读
    • 算术运算符
    • 赋值运算符
    • 关系运算符
    • 逻辑运算符
    • 位运算符
    • 条件运算符
    • 运算符优先级
    • 表达式
  • 第4章 流程控制

    • 章节导读
    • 常见的程序运行流程
    • if-else 选择结构
    • switch 多分支选择
    • while 循环
    • do-while 循环
    • for 循环
    • break 与 continue
  • 第5章 数组

    • 章节导读
    • 一维数组
    • 多维数组
    • Arrays 工具类
  • 第6章 类与对象

    • 章节导读
    • 类与对象
    • 方法定义与调用
    • 构造方法
    • 封装
    • 访问修饰符
    • package 与 import
    • static 关键字
    • this 关键字
    • 参数传递 详解
    • 枚举
    • 成员内部类
    • 局部内部类
    • 静态内部类
    • 匿名内部类
  • 第7章 接口与继承

    • 章节导读
    • 继承
    • super 关键字
    • final 关键字
    • 多态
    • 向上转型与向下转型
    • 抽象类
    • 接口
    • 抽象类与接口对比
  • 第8章 注解

    • 章节导读
    • 注解基础
    • 元注解详解
    • 自定义注解
  • 第9章 常用类

    • 章节导读:Java 常用类
    • Object 类:万类之祖
    • 包装类:基本类型的对象化
    • String:不可变的字符串
    • StringBuffer:线程安全的可变字符串
    • StringBuilder:可变的字符串构建器
    • Math:数学运算工具类
    • Random:伪随机数生成器
    • 大数值运算 详解
    • 日期时间API 详解
  • 第10章 异常机制

    • 章节导读
    • 异常体系与分类
    • try-catch-finally
    • try-with-resources
    • throws 与 throw
    • 自定义异常
  • 第11章 泛型

    • 章节导读
    • 泛型基础
    • 通配符与PECS原则
    • 类型擦除
  • 第12章 集合框架

    • 章节导读
    • 集合框架概述
    • ArrayList
    • LinkedList
    • HashMap 详解
    • LinkedHashMap 详解
    • TreeMap 详解
    • HashSet
    • TreeSet 详解
    • TreeSet 与 Comparable
    • Collections 工具类详解
  • 第13章 IO流

    • 章节导读
    • IO流概述
    • 字节流
    • 字符流
    • 缓冲流
    • 转换流 详解
    • 序列化 详解
    • NIO与Files 详解
    • NIO与Files工具类
  • 第14章 多线程与并发

    • 第十六章 多线程与并发 —— 章节导读
    • 线程基础详解
    • synchronized 详解
    • Lock 与显式锁详解
    • volatile 详解
    • wait 与 notify 详解
    • ThreadLocal详解
    • 原子类详解
    • 并发工具类详解
    • 线程池详解
  • 第15章 反射

    • 章节导读
    • 反射概述与 Class 对象
    • Constructor 与对象创建
    • Field与Method详解
    • 反射应用详解
  • 第16章 JDK8新特性

    • 章节导读
    • Lambda 表达式
    • Stream API 基础
    • Stream API 高级详解
    • Optional 详解
    • 新日期时间API详解
  • 第17章 JDK9-11新特性

    • 章节导读
    • 模块化系统 — Project Jigsaw(JDK 9)
    • var 局部变量类型推断(JDK 10)
    • 集合工厂方法与增强(JDK 9 / 10 / 11)
    • 接口增强:private 方法(JDK 9)
    • Stream API 增强(JDK 9)
    • Optional 增强(JDK 9 / 10 / 11)
    • String 新增方法(JDK 11)
    • HTTP Client 与 Files 增强(JDK 11)
    • 直接运行 Java 源文件 — JEP 330(JDK 11)
  • 第18章 JDK12-17新特性

    • 章节导读
    • Switch 表达式(JDK 12 预览 / JDK 14 正式)
    • 文本块 Text Blocks(JDK 13 预览 / JDK 15 正式)
    • Records 记录类(JDK 14 预览 / JDK 16 正式)
    • 密封类 Sealed Classes(JDK 15 预览 / JDK 17 正式)
    • instanceof 模式匹配(JDK 14 预览 / JDK 16 正式)
    • Switch 模式匹配 — Pattern Matching for switch(JDK 17 预览 / JDK 21 正式)
    • Helpful NPE 与 String 增强(JDK 12 / JDK 14 / JDK 15)
    • Stream 增强(JDK 12 / JDK 16)
    • 日期时间增强 — Day Period 支持(JDK 16)
  • 第19章 JDK18-21新特性

    • 章节导读
    • 虚拟线程(JDK 19 预览 / JDK 20 第二预览 / JDK 21 正式)
    • 序列集合(JDK 21 正式)
    • Switch 模式匹配(JDK 17 预览 / JDK 18 第二预览 / JDK 20 第四预览 / JDK 21 正式)
    • Record 模式匹配(JDK 19 预览 / JDK 20 第二预览 / JDK 21 正式)
    • 未命名模式与变量(JDK 21 预览 / JDK 22 正式)
  • 第20章 JDK 22-25 新特性

    • 章节导读
    • 字符串模板(JDK 22 预览 / JDK 23 第二预览 / JDK 24 第三预览)
    • Stream Gatherers(JDK 22 预览 / JDK 24 第二预览)
    • 隐式声明类与实例方法(JDK 23 预览 / JDK 24 第二预览)
    • 原始类型模式匹配(JDK 24 预览)
  • 附录

    • Java 核心知识点
    • Java SE 专业术语
    • Java特性索引(JDK 8 → 25)

HashMap 详解

本章定位:这是集合框架中最重要的一章。深入理解 HashMap 的哈希表结构(数组+链表+红黑树)、扩容机制(2 倍)、树化条件和 put/get 的完整源码流程。HashMap 是 Java 面试的"必考题",飞翔科技几乎所有键值对存储场景都基于它构建。本章由架构师白歌带领新人小崔深入源码,大翔对性能数据尤为关注,孔蓝则负责验证并发安全性。


定义速览表

属性说明
底层结构Node<K,V>[] table(数组)+ 链表 + 红黑树(JDK 8)
默认初始容量16(DEFAULT_INITIAL_CAPACITY = 1 << 4)
最大容量2^30(MAXIMUM_CAPACITY = 1 << 30)
默认负载因子0.75(DEFAULT_LOAD_FACTOR)
扩容触发条件size > capacity * loadFactor
扩容倍数2 倍(保证容量始终为 2 的幂)
链表 → 红黑树阈值链表长度 ≥ 8 且 数组长度 ≥ 64
红黑树 → 链表阈值树节点数 ≤ 6(resize 时退化)
允许 null 键是(仅一个,hash 值固定为 0)
允许 null 值是
线程安全否(多线程扩容可能数据丢失)
遍历顺序不保证(可能随容量变化而改变)
扩容策略JDK 8 优化:hash & oldCap 判断迁移,无需重新计算 hash
实现接口Map<K,V>, Cloneable, Serializable

底层结构总览

HashMap 在 JDK 8 中的数据结构是一个复合体 —— 数组作为主干,每个桶(bucket)可以是链表或红黑树。这种设计兼顾了查找效率与空间利用。

树化过程状态机

为什么容量必须是 2 的幂?

核心原因:用位运算替代取模运算来定位桶的下标。

// 传统方式:hash % length(取模,效率低)
int index = hash % length;

// HashMap 方式:hash & (length - 1)(位与,效率高)
// 仅在 length = 2^n 时等价于 hash % length
int index = hash & (length - 1);

示例:

length = 16 (0b10000),length - 1 = 15 (0b01111)

hash = 18 (0b10010)
  10010
& 01111
= 00010 → index = 2

hash = 33 (0b100001)
  100001
& 001111  (高位用 0 补齐)
= 000001 → index = 1

位与比取模快一个数量级(1 个 CPU 周期 vs 多个),这是 HashMap 高效的关键设计。


核心原理:put 方法源码全流程分析

完整流程图

关键步骤深入

步骤 1:扰动函数 —— hash 值的计算

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

为什么需要扰动?

hashCode() 返回 32 位整数,但 HashMap 通过 (n-1) & hash 取低几位(例如容量 16 时取低 4 位)。如果 hashCode 的低位分布不均匀,会导致大量冲突。扰动函数将高 16 位与低 16 位异或,使得高位特征也能影响最终的索引计算。

hashCode    = 0001 1101 1010 0011 | 1111 0000 0010 1101
h >>> 16    = 0000 0000 0000 0000 | 0001 1101 1010 0011
XOR         = 0001 1101 1010 0011 | 1110 1101 1000 1110  ← 高低位混合
& (16-1)    =                                     1110  → 索引 14

步骤 2:链表插入 —— 尾插法(JDK 8 vs JDK 7)

// JDK 8: 尾插法
for (int binCount = 0; ; ++binCount) {
    if ((e = p.next) == null) {
        p.next = newNode(hash, key, value, null);  // 追加到尾部
        if (binCount >= TREEIFY_THRESHOLD - 1)
            treeifyBin(tab, hash);  // 达到阈值,树化
        break;
    }
    if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
        break;  // 找到相同 key,替换
    p = e;
}
// JDK 7: 头插法(有死循环风险)
void createEntry(int hash, K key, V value, int bucketIndex) {
    Entry<K,V> e = table[bucketIndex];
    table[bucketIndex] = new Entry<>(hash, key, value, e);  // 新节点插入头部
    size++;
}

JDK 8 改为尾插法的原因:头插法在多线程扩容时可能导致链表成环(死循环)。尾插法虽然在多线程下仍有数据丢失问题,但至少不会形成环形链表。

步骤 3:树化条件 —— 为什么是 8 和 64?

static final int TREEIFY_THRESHOLD = 8;   // 链表长度 ≥ 8 触发树化判断
static final int MIN_TREEIFY_CAPACITY = 64; // 数组长度 ≥ 64 才真正树化

final void treeifyBin(Node<K,V>[] tab, int hash) {
    int n, index; Node<K,V> e;
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        resize();  // 数组太小 → 优先扩容而非树化
    // ... 真正树化
}

为什么阈值是 8?

根据泊松分布,在负载因子 0.75 且 hashCode 均匀分布的情况下,链表长度达到 8 的概率约为 0.00000006(6 千万分之一),几乎不可能发生。一旦发生,说明 hashCode 分布不均(可能是恶意攻击),此时红黑树的 O(log n) 比链表的 O(n) 更有保障。

链表长度出现概率(泊松分布,λ=0.5)
00.60653066
10.30326533
20.07581633
30.01263606
40.00157952
50.00015795
60.00001316
70.00000094
80.00000006

步骤 4:hashCode + equals 契约

// 正确的重写方式(飞翔科技员工类)
class Employee {
    private String id;    // 工号,唯一标识
    private String name;

    @Override
    public int hashCode() {
        return Objects.hash(id);  // 只使用决定唯一性的字段
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Employee)) return false;
        Employee other = (Employee) o;
        return Objects.equals(this.id, other.id);
    }
}

核心契约:

  • 两个对象 equals 返回 true,则 hashCode 必须相等
  • 两个对象 hashCode 相等,equals 不一定返回 true(哈希冲突)
  • 重写 equals 必须同时重写 hashCode,否则 HashMap/HashSet 行为异常

扩容机制:resize() 详解

JDK 8 扩容优化:无需重新计算 hash

这是 JDK 8 对扩容的重大优化。不需要重新计算每个 key 的 hash 值,只需要判断 hash & oldCap 是否为 0:

// JDK 8 resize() 中节点迁移的核心代码
Node<K,V> loHead = null, loTail = null;  // 保持原索引的链表
Node<K,V> hiHead = null, hiTail = null;  // 移动到新索引(index + oldCap)的链表

do {
    next = e.next;
    if ((e.hash & oldCap) == 0) {
        // hash & oldCap == 0 → 索引不变
        if (loTail == null) loHead = e;
        else loTail.next = e;
        loTail = e;
    } else {
        // hash & oldCap != 0 → 索引变为 index + oldCap
        if (hiTail == null) hiHead = e;
        else hiTail.next = e;
        hiTail = e;
    }
} while ((e = next) != null);

newTab[j] = loHead;           // 原位置
newTab[j + oldCap] = hiHead;  // 新位置

原理证明:

扩容前:index = hash & (oldCap - 1)(取低 k 位,oldCap = 2^k) 扩容后:index_new = hash & (newCap - 1)(取低 k+1 位,newCap = 2^(k+1))

第 k+1 位要么是 0(索引不变),要么是 1(索引 = 原索引 + oldCap)。而 hash & oldCap 正好取出了第 k+1 位的值!

例如 oldCap = 16 (0b10000):
  hash1 = 18 (0b10010) → hash1 & oldCap = 0b10010 & 0b10000 = 0 → 索引不变
  hash2 = 33 (0b100001) → hash2 & oldCap = 0b100001 & 0b10000 = 16 → 索引变为 index+16

完整代码示例

示例一:飞翔科技员工信息管理系统

import java.util.*;

/**
 * 场景:飞翔科技的人事系统需要根据工号快速查找员工信息。
 * 大翔要求系统在 10 万员工规模下,查询响应时间不超过 1ms。
 * 白歌选择了 HashMap,小崔负责实现基本功能,孔蓝负责边界测试。
 */
public class FeiXiangEmployeeManager {
    // ------ 员工实体(不可变,适合做 key) ------
    static class Employee {
        private final String id;   // 工号(唯一标识)
        private final String name;
        private final String department;
        private final double salary;

        public Employee(String id, String name, String department, double salary) {
            this.id = id;
            this.name = name;
            this.department = department;
            this.salary = salary;
        }

        // hashCode 和 equals 只依赖 id —— 工号决定了员工身份
        @Override
        public int hashCode() {
            return Objects.hash(id);
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (!(o instanceof Employee)) return false;
            Employee employee = (Employee) o;
            return Objects.equals(id, employee.id);
        }

        public String getId() { return id; }
        public String getName() { return name; }

        @Override
        public String toString() {
            return String.format("%s | %s | %s | ¥%.0f", id, name, department, salary);
        }
    }

    public static void main(String[] args) {
        // ========== 1. 创建 HashMap ==========
        HashMap<String, Employee> employeeMap = new HashMap<>(16, 0.75f);

        // ========== 2. put:录入员工信息 ==========
        employeeMap.put("EMP001", new Employee("EMP001", "大翔", "管理层", 80000));
        employeeMap.put("EMP002", new Employee("EMP002", "白歌", "技术部", 35000));
        employeeMap.put("EMP003", new Employee("EMP003", "小崔", "技术部", 12000));
        employeeMap.put("EMP004", new Employee("EMP004", "孔蓝", "测试部", 15000));
        employeeMap.put("EMP005", new Employee("EMP005", "Frank", "市场部", 22000));
        employeeMap.put("EMP006", new Employee("EMP006", "黄俪", "人事部", 18000));

        System.out.println("=== 飞翔科技员工信息管理系统 ===");
        System.out.println("员工总数: " + employeeMap.size());

        // ========== 3. get:根据工号查询 ==========
        System.out.println("\n--- 工号查询 ---");
        System.out.println("EMP001: " + employeeMap.get("EMP001"));
        System.out.println("EMP003: " + employeeMap.get("EMP003"));
        System.out.println("EMP999(不存在): " + employeeMap.get("EMP999"));

        // 安全的获取方式(JDK 8)
        Employee defaultEmp = new Employee("UNKNOWN", "未知", "无", 0);
        System.out.println("EMP002 安全获取: " + employeeMap.getOrDefault("EMP002", defaultEmp));
        System.out.println("EMP999 安全获取: " + employeeMap.getOrDefault("EMP999", defaultEmp));

        // ========== 4. containsKey / containsValue ==========
        System.out.println("\n--- 存在性判断 ---");
        System.out.println("包含工号 EMP001? " + employeeMap.containsKey("EMP001"));
        System.out.println("包含工号 EMP999? " + employeeMap.containsKey("EMP999"));

        // ========== 5. 遍历方式对比 ==========
        System.out.println("\n=== 遍历方式 ===");

        // 方式一:entrySet —— 推荐,一次遍历拿到 key 和 value
        System.out.println("--- entrySet(推荐) ---");
        for (Map.Entry<String, Employee> entry : employeeMap.entrySet()) {
            System.out.println("  " + entry.getKey() + " → " + entry.getValue().getName());
        }

        // 方式二:JDK 8 forEach + Lambda —— 最简洁
        System.out.println("\n--- forEach(JDK 8 Lambda) ---");
        employeeMap.forEach((id, emp) ->
            System.out.println("  " + id + " → " + emp.getName())
        );

        // 方式三:keySet + get —— 不推荐(每次 get 都是一次查找)
        System.out.println("\n--- keySet + get(不推荐,多一次查找) ---");
        for (String id : employeeMap.keySet()) {
            System.out.println("  " + id + " → " + employeeMap.get(id).getName());
        }

        // ========== 6. 替换操作 ==========
        System.out.println("\n--- 替换操作 ---");
        employeeMap.replace("EMP003", new Employee("EMP003", "小崔(已晋升)", "技术部", 20000));
        System.out.println("替换后 EMP003: " + employeeMap.get("EMP003").getName());

        // 仅当旧值匹配时才替换(replace(K, V, V))
        boolean replaced = employeeMap.replace("EMP004",
            employeeMap.get("EMP004"),
            new Employee("EMP004", "孔蓝(高级)", "测试部", 20000));
        System.out.println("条件替换结果: " + replaced);

        // ========== 7. 删除 ==========
        Employee removed = employeeMap.remove("EMP006");
        System.out.println("\n删除 EMP006: " + (removed != null ? removed.getName() : "无"));
        System.out.println("删除后员工总数: " + employeeMap.size());

        // ========== 8. 统计:技术部员工 ==========
        long techCount = employeeMap.values().stream()
            .filter(e -> "技术部".equals(e.department))
            .count();
        System.out.println("\n技术部员工数: " + techCount);
    }
}
=== 飞翔科技员工信息管理系统 ===
员工总数: 6

--- 工号查询 ---
EMP001: EMP001 | 大翔 | 管理层 | ¥80000
EMP003: EMP003 | 小崔 | 技术部 | ¥12000
EMP999(不存在): null
EMP002 安全获取: EMP002 | 白歌 | 技术部 | ¥35000
EMP999 安全获取: UNKNOWN | 未知 | 无 | ¥0

--- 存在性判断 ---
包含工号 EMP001? true
包含工号 EMP999? false

=== 遍历方式 ===
--- entrySet(推荐) ---
  EMP005 → Frank
  EMP006 → 黄俪
  EMP003 → 小崔
  EMP004 → 孔蓝
  EMP001 → 大翔
  EMP002 → 白歌

--- forEach(JDK 8 Lambda) ---
  EMP005 → Frank
  EMP006 → 黄俪
  EMP003 → 小崔
  EMP004 → 孔蓝
  EMP001 → 大翔
  EMP002 → 白歌

--- keySet + get(不推荐,多一次查找) ---
  EMP005 → Frank
  EMP006 → 黄俪
  EMP003 → 小崔
  EMP004 → 孔蓝
  EMP001 → 大翔
  EMP002 → 白歌

--- 替换操作 ---
替换后 EMP003: 小崔(已晋升)
条件替换结果: true

删除 EMP006: 黄俪
删除后员工总数: 5

技术部员工数: 2

示例二:HashMap 扩容与树化行为验证

import java.util.*;

/**
 * 场景:白歌让小崔验证 HashMap 的树化过程和扩容行为。
 * 大翔看到验证结果后,对 JDK 8 的优化表示认可。
 */
class PoorHashKey {
    private final int id;

    public PoorHashKey(int id) { this.id = id; }

    // 故意制造哈希冲突:所有对象返回相同的 hashCode
    @Override
    public int hashCode() { return 1; }  // 全部落入同一桶!

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof PoorHashKey)) return false;
        return id == ((PoorHashKey) o).id;
    }

    @Override
    public String toString() { return "Key#" + id; }
}

public class HashMapTreeifyDemo {
    public static void main(String[] args) {
        // ========== 演示一:链表 → 红黑树 ==========
        System.out.println("=== 演示一:链表增长 → 触发树化 ===");
        HashMap<PoorHashKey, String> map = new HashMap<>();

        for (int i = 1; i <= 12; i++) {
            map.put(new PoorHashKey(i), "Value" + i);
            if (i == 8) {
                System.out.println("  添加第 " + i + " 个元素(达到 TREEIFY_THRESHOLD = 8)");
            }
            if (i == 9) {
                System.out.println("  添加第 " + i + " 个元素(容量 >= 64 时链表转为红黑树)");
            }
        }
        System.out.println("  最终 size: " + map.size());
        System.out.println("  所有元素在同一桶中(hashCode 均为 1)");

        // 验证数据完整性
        for (int i = 1; i <= 12; i++) {
            String value = map.get(new PoorHashKey(i));
            System.out.println("    get(Key#" + i + ") = " + value);
        }

        // ========== 演示二:扩容阈值观察 ==========
        System.out.println("\n=== 演示二:负载因子 0.75 下的扩容行为 ===");
        HashMap<Integer, String> demo = new HashMap<>(4, 0.75f);
        int lastCap = getCapacity(demo);

        System.out.println("初始容量: " + lastCap + ",触发扩容阈值: " + (int)(lastCap * 0.75));

        for (int i = 0; i < 30; i++) {
            demo.put(i, "V" + i);
            int currentCap = getCapacity(demo);
            if (currentCap != lastCap) {
                System.out.println("  size=" + demo.size()
                    + " → 扩容: " + lastCap + " → " + currentCap
                    + " (threshold=" + (int)(currentCap * 0.75) + ")");
                lastCap = currentCap;
            }
        }

        System.out.println("\n最终容量: " + getCapacity(demo) + ",元素数: " + demo.size());

        // ========== 演示三:JDK 8 putIfAbsent / computeIfAbsent ==========
        System.out.println("\n=== 演示三:JDK 8 新增方法 ===");

        HashMap<String, List<String>> deptMap = new HashMap<>();

        // computeIfAbsent:键不存在时才计算并插入
        deptMap.computeIfAbsent("技术部", k -> new ArrayList<>()).add("白歌");
        deptMap.computeIfAbsent("技术部", k -> new ArrayList<>()).add("小崔");
        deptMap.computeIfAbsent("测试部", k -> new ArrayList<>()).add("孔蓝");
        deptMap.computeIfAbsent("市场部", k -> new ArrayList<>()).add("Frank");

        deptMap.forEach((dept, members) ->
            System.out.println("  " + dept + ": " + members)
        );

        // merge:合并值
        HashMap<String, Integer> salaryMap = new HashMap<>();
        salaryMap.put("技术部", 35000);
        salaryMap.merge("技术部", 12000, Integer::sum);
        salaryMap.merge("测试部", 15000, Integer::sum);
        System.out.println("\n部门工资汇总: " + salaryMap);
    }

    /** 通过反射获取 HashMap 内部 table 数组的长度 */
    private static int getCapacity(HashMap<?, ?> map) {
        try {
            java.lang.reflect.Field field = HashMap.class.getDeclaredField("table");
            field.setAccessible(true);
            Object[] table = (Object[]) field.get(map);
            return table == null ? 0 : table.length;
        } catch (Exception e) {
            return -1;
        }
    }
}
=== 演示一:链表增长 → 触发树化 ===
  添加第 8 个元素(达到 TREEIFY_THRESHOLD = 8)
  添加第 9 个元素(容量 >= 64 时链表转为红黑树)
  最终 size: 12
  所有元素在同一桶中(hashCode 均为 1)
    get(Key#1) = Value1
    get(Key#2) = Value2
    get(Key#3) = Value3
    get(Key#4) = Value4
    get(Key#5) = Value5
    get(Key#6) = Value6
    get(Key#7) = Value7
    get(Key#8) = Value8
    get(Key#9) = Value9
    get(Key#10) = Value10
    get(Key#11) = Value11
    get(Key#12) = Value12

=== 演示二:负载因子 0.75 下的扩容行为 ===
初始容量: 4,触发扩容阈值: 3
  size=4 → 扩容: 4 → 8 (threshold=6)
  size=7 → 扩容: 8 → 16 (threshold=12)
  size=13 → 扩容: 16 → 32 (threshold=24)
  size=25 → 扩容: 32 → 64 (threshold=48)

最终容量: 64,元素数: 30

=== 演示三:JDK 8 新增方法 ===
  技术部: [白歌, 小崔]
  测试部: [孔蓝]
  市场部: [Frank]

部门工资汇总: {技术部=47000, 测试部=15000}

易错场景

反例一:可变对象作为 key

小崔在员工缓存中踩的经典坑 —— 修改了 key 的 hashCode 后数据丢失:

// ❌ 错误:将可变对象作为 HashMap 的 key,修改后无法找到
class MutableKey {
    String id;
    MutableKey(String id) { this.id = id; }

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

    @Override
    public boolean equals(Object o) {
        if (!(o instanceof MutableKey)) return false;
        return Objects.equals(id, ((MutableKey) o).id);
    }
}

HashMap<MutableKey, String> cache = new HashMap<>();
MutableKey key = new MutableKey("user_001");
cache.put(key, "小崔的信息");

key.id = "user_002";  // 修改了 key 的 hashCode!
System.out.println(cache.get(key));  // null!数据丢失!
// 甚至无法 remove(key),造成"内存泄漏"

纠正:使用 String、Integer 等不可变类作为 key,或确保 key 对象的 hashCode 依赖字段不可变。

// ✅ 正确:使用不可变类作为 key
String key = "user_001";
cache.put(key, "小崔的信息");
// String 不可变,hashCode 永远不会变化

反例二:多线程 put 导致数据覆盖

// ❌ 错误:多线程并发 put 导致数据丢失
HashMap<Integer, String> map = new HashMap<>();
CountDownLatch latch = new CountDownLatch(2);

new Thread(() -> {
    for (int i = 0; i < 1000; i++) map.put(i, "A");
    latch.countDown();
}).start();

new Thread(() -> {
    for (int i = 0; i < 1000; i++) map.put(i, "B");
    latch.countDown();
}).start();

latch.await();
System.out.println("期望: 1000, 实际: " + map.size());  // 可能 < 1000 甚至异常

纠正:

// ✅ 方案一:使用 ConcurrentHashMap(推荐)
ConcurrentHashMap<Integer, String> concurrentMap = new ConcurrentHashMap<>();
// 内部使用 CAS + synchronized,线程安全且高效

// ✅ 方案二:使用 Collections.synchronizedMap
Map<Integer, String> syncMap = Collections.synchronizedMap(new HashMap<>());
// 所有操作都有 synchronized 保护,但遍历时需手动加锁

反例三:重写 equals 但不重写 hashCode

// ❌ 错误:只重写了 equals,未重写 hashCode
class BadEmployee {
    String id;
    String name;

    BadEmployee(String id, String name) {
        this.id = id;
        this.name = name;
    }

    @Override
    public boolean equals(Object o) {
        if (!(o instanceof BadEmployee)) return false;
        BadEmployee e = (BadEmployee) o;
        return Objects.equals(id, e.id);
    }
    // 未重写 hashCode!继承 Object 的 hashCode(基于内存地址)
}

// 测试
HashMap<BadEmployee, String> map = new HashMap<>();
BadEmployee e1 = new BadEmployee("001", "小崔");
map.put(e1, "小崔的信息");

BadEmployee e2 = new BadEmployee("001", "小崔");
System.out.println(e1.equals(e2));         // true —— equals 认为相等
System.out.println(map.get(e2));            // null —— hashCode 不同,找不到!
System.out.println(e1.hashCode() == e2.hashCode()); // false —— 违反了契约!

纠正:

// ✅ 正确:equals 和 hashCode 保持一致性
class GoodEmployee {
    String id;
    String name;

    GoodEmployee(String id, String name) {
        this.id = id;
        this.name = name;
    }

    @Override
    public boolean equals(Object o) {
        if (!(o instanceof GoodEmployee)) return false;
        GoodEmployee e = (GoodEmployee) o;
        return Objects.equals(id, e.id);
    }

    @Override
    public int hashCode() {
        return Objects.hash(id);  // 使用与 equals 相同的字段
    }
}

反例四:遍历时删除元素不通过迭代器

// ❌ 错误:直接使用 map.remove 会抛 ConcurrentModificationException
HashMap<String, String> map = new HashMap<>();
map.put("A", "1"); map.put("B", "2"); map.put("C", "3");

for (Map.Entry<String, String> entry : map.entrySet()) {
    if ("B".equals(entry.getKey())) {
        map.remove(entry.getKey());  // ConcurrentModificationException!
    }
}

纠正:

// ✅ 使用迭代器的 remove 方法
Iterator<Map.Entry<String, String>> it = map.entrySet().iterator();
while (it.hasNext()) {
    Map.Entry<String, String> entry = it.next();
    if ("B".equals(entry.getKey())) {
        it.remove();  // 安全删除
    }
}

// ✅ JDK 8 更简洁的方式
map.entrySet().removeIf(entry -> "B".equals(entry.getKey()));

面试考点

Q1:HashMap 的底层数据结构是怎样的?JDK 7 和 JDK 8 有什么区别?

HashMap 在 JDK 8 中使用数组 + 链表 + 红黑树的复合结构。数组是主干,每个桶可以存链表或红黑树。JDK 7 仅使用数组 + 链表。主要区别: ① 树化:JDK 8 链表长度 ≥ 8 且数组长度 ≥ 64 时转换为红黑树(最坏 O(n) → O(log n)); ② 插入方式:JDK 7 头插法 → JDK 8 尾插法(解决多线程扩容死循环); ③ hash 扰动:JDK 7 扰动 4 次 → JDK 8 扰动 1 次(效率与效果的平衡); ④ 扩容迁移:JDK 7 需要重新计算每个 key 的 hash → JDK 8 通过 hash & oldCap 判断,无需重算。

Q2:HashMap 的 put 方法完整流程是怎样的?

① 调用 hash(Object key) 计算 hash 值:(h = key.hashCode()) ^ (h >>> 16); ② 判断 table 是否为 null 或 length 为 0,若是则 resize() 初始化(默认容量 16); ③ 计算桶下标:index = (n - 1) & hash; ④ 若桶为空,直接放入新 Node; ⑤ 若桶中首个节点是 TreeNode,走红黑树插入逻辑 putTreeVal(); ⑥ 否则遍历链表:找到相同 key 则替换 value;遍历到尾节点则尾插法追加;追加后若链表长度 ≥ 8,触发 treeifyBin(); ⑦ treeifyBin() 中:若数组长度 < 64 则优先扩容;否则链表转红黑树; ⑧ 插入后 size++,若 size > threshold(capacity * loadFactor),触发 resize() 扩容。

Q3:为什么 HashMap 的容量必须是 2 的幂?负载因子为什么是 0.75?

容量为 2 的幂:为了用 hash & (length - 1) 替代 hash % length。取模运算需要 CPU 做除法(数十个周期),而位与运算仅需 1 个 CPU 周期。但该等价关系仅在 length 为 2 的幂时成立。此外,扩容时通过 hash & oldCap 就能确定新位置,无需重新计算 hash。

负载因子 0.75:这是时间与空间的工程权衡。负载因子越大,空间利用率越高(桶塞得越满),但哈希冲突概率越大(查找退化);负载因子越小,查找越快但空间浪费越大。0.75 在泊松分布下,桶中元素数量分布最为理想(链表长度 ≥ 8 的概率仅为千万分之六)。

Q4:HashMap 为什么线程不安全?具体表现有哪些?如何在并发场景下正确使用?

线程不安全的 4 种表现: ① JDK 7 扩容死循环:头插法 resize 时链表可能成环,get() 进入死循环 CPU 100%(JDK 8 尾插法修复); ② 数据覆盖:两个线程同时 put 到同一桶,后一个覆盖前一个(put 非原子操作); ③ size 计数错误:size++ 非原子操作,多线程自增导致计数不准; ④ 扩容时数据丢失:线程 A 迁移节点时,线程 B 将节点放入已迁移的桶,导致节点丢失。

并发方案: ① ConcurrentHashMap(首选):JDK 8 使用 CAS + synchronized,锁粒度桶级别,高并发性能最优; ② Collections.synchronizedMap:方法级 synchronized,性能较低但实现简单; ③ Hashtable:遗留类(JDK 1.0),不推荐新代码使用。

Q5:HashMap 与 Hashtable 的区别?

维度HashMapHashtable
线程安全否是(synchronized 方法)
null 键/值允许各一个不允许
父类AbstractMapDictionary(遗留)
迭代器fail-fast Iteratorfail-fast Iterator + Enumeration
出现时间JDK 1.2JDK 1.0
默认容量1611
扩容2 倍2 倍 + 1
效率高(无锁)低(方法级锁)

Q6:HashMap 的 keySet / values / entrySet 返回的是独立副本吗?

都不是独立副本,而是视图(view)。对视图的修改会直接影响 HashMap,反之亦然。例如通过 keySet().remove(key) 删除键,会同时删除 HashMap 中的对应条目。如果需要在遍历时安全修改,应使用迭代器的 remove() 方法或 JDK 8 的 removeIf()。这三个视图的迭代器都是 fail-fast 的——在迭代过程中若 HashMap 发生结构性修改(非迭代器自身操作),会抛出 ConcurrentModificationException。

上一页
LinkedList
下一页
LinkedHashMap 详解