当前位置: 首页 > news >正文

常见集合篇(五)深入解析 HashMap:从原理到源码,全方位解读

常见集合篇(五):深入解析 HashMap,从原理到源码,全方位解读

  • 常见集合篇(五):深入解析 HashMap,从原理到源码,全方位解读
    • 引言
    • 一、HashMap 的实现原理
      • 1.1 数据结构基础
      • 1.2 存储逻辑示例
    • 二、HashMap 的 `put` 方法具体流程
      • 2.1 JDK 8 源码分析
      • 2.2 执行步骤拆解
    • 三、HashMap 常见属性
    • 四、HashMap 的扩容机制
      • 4.1 触发条件
      • 4.2 扩容过程
      • 4.3 示例
    • 五、HashMap 的寻址算法
      • 5.1 哈希计算
      • 5.2 桶定位
    • 六、HashMap 在 1.7 下的多线程死循环问题
      • 6.1 问题根源
      • 6.2 示例场景
    • 七、HashSet 与 HashMap 的区别
    • 八、HashTable 与 HashMap 的区别
    • 总结

常见集合篇(五):深入解析 HashMap,从原理到源码,全方位解读

引言

在 Java 编程领域,HashMap 是一种高频使用的数据结构,无论是日常开发中的键值对存储,还是面试中的核心考点,它都占据着重要地位。本文将围绕 HashMap 的实现原理、put 方法流程、常见属性、扩容机制、寻址算法等核心问题展开深度剖析,同时对比 HashSet、HashTable 的差异,帮助读者全面掌握这一关键数据结构。


一、HashMap 的实现原理

1.1 数据结构基础

HashMap 采用 “数组 + 链表 + 红黑树” 的复合结构:

  • 数组:作为底层存储,每个数组元素称为一个“桶”(Bucket)。

  • 链表:当多个键值对的哈希值映射到同一桶时,通过链表解决冲突。

  • 红黑树:当链表长度超过阈值(默认 8),链表会转为红黑树,提升查询效率(JDK 8 新增优化)。

  • 冲突处理:由于不同的键可能具有相同的哈希值,这就会导致冲突。当发生冲突时,HashMap使用链表或红黑树等数据结构来存储具有相同哈希值的键值对。这些数据结构允许在冲突的位置上存储多个键值对,并通过比较键的equals()方法来区分它们

    链表:在JDK 8之前,HashMap使用链表来解决冲突。当多个键值对被映射到同一个桶时,它们会形成一个链表。通过遍历链表来查找、插入或删除键值对。但是,当链表长度过长时,会影响HashMap的性能

    红黑树:从JDK 8开始,当链表长度超过一个阈值(默认为8)时,链表会被自动转换为红黑树,以提高操作效率。红黑树的查找、插入和删除操作具有较低的时间复杂度,可以在平均情况下保持对数时间复杂度

1.2 存储逻辑示例

假设存储键值对 {“key1”, “value1”}

  1. 计算 key1 的哈希值,确定其在数组中的桶位置。
  2. 若桶为空,直接将键值对存入桶对应的链表头。
  3. 若桶已存在元素,遍历链表(或红黑树),对比键是否相等:
    • 相等则覆盖值;
    • 不相等则新增节点(若链表长度达标,转换为红黑树)。

二、HashMap 的 put 方法具体流程

2.1 JDK 8 源码分析

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    // 1. 初始化数组
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    // 2. 定位桶,若桶空则新建节点
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;
        // 3. 桶首节点匹配
        if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        // 4. 红黑树处理
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            // 5. 遍历链表
            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;
                p = e;
            }
        }
        if (e != null) { // 覆盖值
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            return oldValue;
        }
    }
    ++modCount;
    // 6. 检查扩容
    if (++size > threshold)
        resize();
    return null;
}

2.2 执行步骤拆解

  1. 计算哈希:通过 hash(key) 对键进行哈希处理,减少哈希冲突。
  2. 初始化数组:若数组未初始化,调用 resize() 创建默认容量(16)的数组。
  3. 定位桶位置:通过 (n - 1) & hash 计算桶索引。
  4. 处理冲突
    • 桶为空:直接插入新节点。
    • 桶非空:检查桶首节点是否匹配,匹配则覆盖值;若为红黑树,调用红黑树插入方法;否则遍历链表,插入新节点(若链表过长,转换为红黑树)。
  5. 检查扩容:若元素数量超过阈值,触发扩容。

三、HashMap 常见属性

属性名称类型默认值说明
DEFAULT_INITIAL_CAPACITYint16默认初始容量,必须是 2 的幂次。
DEFAULT_LOAD_FACTORfloat0.75f默认负载因子,用于计算扩容阈值。
thresholdint-扩容阈值,值为 容量 × 负载因子
loadFactorfloat-负载因子,控制扩容时机,影响空间和时间效率。
modCountint-记录集合结构修改次数,用于 ConcurrentModificationException 快速失败机制。

四、HashMap 的扩容机制

4.1 触发条件

size(当前元素数量)超过 threshold(扩容阈值,即 容量 × 负载因子)时,触发扩容。

4.2 扩容过程

  1. 创建新数组:新数组容量为旧数组的 2 倍,例如旧容量 16,新容量 32。
  2. 迁移元素:遍历旧数组每个桶,重新计算元素在新数组的位置并复制。由于容量是 2 的幂次,元素新位置要么在原位置,要么在原位置 + 旧容量。

4.3 示例

假设初始容量为 16,负载因子 0.75,阈值为 12。当添加第 13 个元素时:

  1. 触发扩容,创建容量为 32 的新数组。
  2. 遍历旧数组,每个元素重新计算哈希:
    • 若元素原桶索引为 i,新索引可能是 ii + 16
    • 例如,旧数组中某元素哈希值与 15(16-1)按位与得 5,扩容后与 31(32-1)按位与,若高位变化,新索引为 5 + 16 = 21。

五、HashMap 的寻址算法

5.1 哈希计算

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
  • 目的:将哈希码的高 16 位与低 16 位异或,减少哈希冲突。
  • 示例:若 key.hashCode()0b10101100,右移 16 位后与原值异或,混合高低位信息。

5.2 桶定位

通过 (n - 1) & hash 计算桶索引,其中 n 是数组长度(且为 2 的幂次)。例如,数组长度 16(二进制 10000),n - 11501111),与哈希值按位与,确保结果在数组范围内,且分布均匀。


六、HashMap 在 1.7 下的多线程死循环问题

6.1 问题根源

JDK 1.7 中,HashMap 扩容采用头插法。多线程环境下,若线程 A 和线程 B 同时扩容,可能导致链表节点顺序混乱,形成循环链表。

6.2 示例场景

  1. 初始链表:节点 AB,存储在旧数组桶中。
  2. 线程 A 扩容:复制 B 到新数组,再复制 A,新链表为 AB
  3. 线程 B 同时扩容:同样复制节点,但因并发操作,链表可能变为 ABA,形成循环。
  4. 查询触发死循环:遍历链表时,因循环结构导致无限循环。

七、HashSet 与 HashMap 的区别

对比维度HashSetHashMap
存储内容仅存储键,值为固定对象 PRESENT存储键值对
实现原理基于 HashMap,复用 HashMap 的键存储逻辑独立实现键值对存储
核心方法add()remove() 等键操作put()get() 等键值对操作
空值支持允许存储一个 null允许 null 键和 null 值(键唯一)

示例

HashSet<String> set = new HashSet<>();
set.add("test"); // 内部调用 HashMap 的 put(key, PRESENT)

HashMap<String, Integer> map = new HashMap<>();
map.put("key", 1); // 存储键值对

八、HashTable 与 HashMap 的区别

对比维度HashTableHashMap
线程安全方法加 synchronized,线程安全非线程安全
空值支持键和值均不能为 null允许 null 键和 null 值(键唯一)
性能同步开销大,性能较低无同步开销,性能更优
继承体系继承 Dictionary实现 Map 接口
扩容机制扩容为原容量 2 倍 + 1扩容为原容量 2 倍

示例

HashTable<String, Integer> table = new HashTable<>();
// table.put(null, 1); // 编译报错,不允许 null 键
// table.get(null); // 编译报错,不允许 null 键

HashMap<String, Integer> map = new HashMap<>();
map.put(null, 1); // 允许 null 键
map.put("key", null); // 允许 null 值

总结

通过对 HashMap 实现原理、put 流程、属性、扩容机制、寻址算法的深入分析,以及与 HashSet、HashTable 的对比,我们全面掌握了这一数据结构的核心要点。在实际开发中,需根据场景选择合适的集合:单线程环境优先用 HashMap;需要线程安全时,可选择同步包装类 Collections.synchronizedMapConcurrentHashMap;而 HashTable 因性能问题已逐渐被替代。理解这些细节,不仅能写出更高效的代码,也能在面试中从容应对相关问题。

相关文章:

  • 青铜与信隼的史诗——TCP与UDP的千年博弈
  • 【JavaScript】闭包小练习(数字范围起始值和结束值)
  • RHCSA Linux 系统创建文件
  • Vim操作指令全解析
  • 质检LIMS实验室系统在环保技术企业的应用 环保技术研发场景的特殊性需求
  • C++高效读取大规模文本格式点云(windows)
  • 手机归属地查询Api接口,数据准确可靠
  • 根据时间范围得出雪花算法(snowflake)ID的工具类-基于时间反推ID范围
  • AiCube 试用 - 创建流水灯工程
  • 有瓶颈设备的多级生产计划问题:基于Matlab的深度解析与实践
  • LeetCode 解题思路 31(Hot 100)
  • 八. 深入理解 Java 继承:概念、应用与最佳实践
  • Error:java: 程序包lombok不存在
  • 基于springboot+vue的停车场管理系统
  • 数据库 第一章 MYSQL基础(5)
  • 在线Notepad智能笔记——你的全能AI创作助手
  • C++ 常量
  • 【gdutthesis模板】章节标题有英文解决方案
  • 数据结构第一轮复习--第六章图包含代码
  • 蓝桥杯冲刺