news 2026/10/15 16:37:23

Java HashMap全面解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Java HashMap全面解析

HashMap 是 Java 集合框架中最常用的键值对(Key-Value)存储容器;同时在安卓开发中,HashMap 是本地数据存储、临时缓存的核心工具。接下来我们来看看 HashMap 的定义、底层结构、核心算法、扩容机制、线程安全问题。

一、HashMap定义

HashMap 是java.util包下的实现类,实现了Map<K,V>接口,基于哈希表实现键值对存储,核心特征:

  • 存储规则:键(Key)唯一,值(Value)可重复;键和值都允许为null;
  • 无序性:不保证插入顺序与遍历顺序一致;
  • 非线程安全:HashMap 为了机制的性能,所有核心操作都没有任何同步或原子性保障,多线程并发时应使用 ConcurrentHashMap 或 synchronizedMap;
  • 性能:增删改查平均时间复杂度为O(1),哈希冲突严重时退化为O(n)(JDK1.8 后优化为O(logn))

二、HashMap 底层数据结构

在JDK1.8之后 HashMap 的底层结构经历了关键优化,目的是解决“哈希冲突导致链表过长”的性能问题。

JDK1.7:数组(桶)+ 链表

JDK1.8之前 HashMap 底层是数组和链表结合在一起使用的。HashMap 通过 key 的 hashcode 经过扰动函数(用来优化哈希值的分布,减少碰撞)处理后得到 hash值,然后通过寻址算法判断当前元素存放的位置(前提是数组长度 n 必须是 2 的幂次)。

  • 数组(Hash 桶):核心存储结构,数组的每个元素是一个链表的头节点,数组下标通过 Key 的哈希值计算得到;
  • 链表:解决哈希冲突(不同 Key 计算出相同下标),冲突的键值对以链表形式挂在同一个数组下标下。
  • 缺点:当链表过长时,查询效率从O(1)退化为O(n)。

JDK1.8:数组(桶)+ 链表 + 红黑树

JDK1.8之后在解决哈希冲突时有了较大的变化,当链表长度大于8且数组总长度大于64时,链表转化为红黑树,查询复杂度从O(n)降至O(logn)。

当红黑树节点数减少到6时,会重新退化为链表。

三、HashMap 的基本使用

HashMap 的 API 简单易用,核心操作包括「增、删、改、查、遍历」,简单示例如下:

import java.util.HashMap; import java.util.Iterator; import java.util.Map; public class HashMapBasicUsage { public static void main(String[] args) { // 创建HashMap:指定键值类型,默认初始容量16,负载因子0.75 HashMap<String, Integer> userScore = new HashMap<>(); // 也可指定初始容量和负载因子:HashMap<String, Integer> map = new HashMap<>(32, 0.75f); // 新增元素(put) userScore.put("张三", 90); userScore.put("李四", 85); userScore.put("王五", 95); userScore.put(null, 0); // 键允许为null userScore.put("赵六", null); // 值允许为null // 修改元素(重复put同一键,覆盖值) userScore.put("李四", 88); // 查询元素 // get(key):存在返回值,不存在返回null Integer zhangSanScore = userScore.get("张三"); System.out.println("张三的分数:" + zhangSanScore); // 输出:90 // containsKey(key):判断键是否存在 boolean hasWangWu = userScore.containsKey("王五"); System.out.println("是否包含王五:" + hasWangWu); // 输出:true // containsValue(value):判断值是否存在(效率低,需遍历) boolean hasScore95 = userScore.containsValue(95); System.out.println("是否有95分:" + hasScore95); // 输出:true // 删除元素 userScore.remove(null); // 删除键为null的元素 userScore.remove("赵六"); // 删除键为赵六的元素 // 遍历HashMap(三种常用方式) System.out.println("\n=== 遍历方式1:entrySet(推荐,效率最高)==="); for (Map.Entry<String, Integer> entry : userScore.entrySet()) { System.out.println("键:" + entry.getKey() + ",值:" + entry.getValue()); } System.out.println("\n=== 遍历方式2:keySet + get(效率低,需二次哈希)==="); for (String key : userScore.keySet()) { System.out.println("键:" + key + ",值:" + userScore.get(key)); } System.out.println("\n=== 遍历方式3:迭代器(支持删除)==="); Iterator<Map.Entry<String, Integer>> iterator = userScore.entrySet().iterator(); while (iterator.hasNext()) { Map.Entry<String, Integer> entry = iterator.next(); if (entry.getValue() >= 90) { System.out.println("高分用户:" + entry.getKey()); // 遍历中删除元素,只能用迭代器的remove(否则抛ConcurrentModificationException) // iterator.remove(); } } // 其他常用方法 System.out.println("\nHashMap大小:" + userScore.size()); // 输出:3 userScore.clear(); // 清空所有元素 System.out.println("清空后是否为空:" + userScore.isEmpty()); // 输出:true } }

四、HashMap的核心算法

哈希算法

我们只看JDK1.8的 hash 算法:

static final int hash(Object key) { int h; // 1. key为null时,哈希值为0;2. 非null时,取hashCode()并与高16位异或 return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }

将 Key 的 hashCode 高 16 位与低 16 位异或,让高位特征融入低位 —— 因为数组下标计算只用到低位,这样能减少哈希冲突。

数组下标计算

就是之前提到的寻址算法,实现如下:

index = hash & (length - 1);
  • hash & (length - 1)在length为 2 的幂时,等价于对length的取模运算,但位运算比取模快得多(CPU 原生支持);
  • length是 2 的幂保证length - 1的二进制全为 1,这样hash & (length - 1)能充分利用哈希值的低位,分散元素分布,减少冲突。

哈希冲突解决

当两个 Key 计算出相同下标时,HashMap 采用拉链法解决:

  • JDK1.7:新元素以头插法插入链表(扩容时可能导致链表循环,线程不安全);
  • JDK1.8:新元素以尾插法插入链表(解决头插法的循环问题,仍非线程安全);
  • 当链表长度≥8 且数组长度≥64 时,链表转为红黑树,提升查询效率。

五、HashMap 扩容机制

当 HashMap 新增元素后里面的元素个数 > 阈值时,就会触发扩容。

扩容相关的参数:

  • initialCapacity:初始容量;
  • loadFactor:负载因子(扩容阈值比例),默认为0.75;
  • threshold:扩容阈值,值为容量×负载因子。

扩容过程:

1. 创建新数组,容量 = 旧容量 × 2,阈值 = 旧阈值 × 2;

2. 遍历原数组中的每一个桶(Bucket),根据桶内结点的状态进行分类处理:

  • 空桶:直接跳过;
  • 单节点(无冲突):重新计算下标newTab[e.hash & (newCap - 1)] = e;
  • 链表:判断e.hash & oldCap,为0时元素下标不变,否则下标 = 旧下标 + oldCap;
  • 红黑树:先拆分(逻辑同链表,拆分成两个子树),如果拆分后的子树节点数 ≤ 6,则将该树转回普通链表,否则将重新修剪红黑树放入对应位置。

3. 将内部成员变量 table 指向这个新数组,新数组替代旧数组,旧数组等待下一次 GC 回收。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/15 8:54:07

USB协议栈的‘隐藏关卡’:那些手册没告诉你的设计哲学

USB协议栈的深层设计哲学&#xff1a;从STM32H7实战看协议栈实现的艺术 当我们在STM32H7上实现USB功能时&#xff0c;往往只关注如何调用现成的协议栈API&#xff0c;却忽略了协议栈底层精妙的设计逻辑。本文将带您深入USB协议栈的实现细节&#xff0c;揭示那些手册中未曾明言…

作者头像 李华
网站建设 2026/10/15 14:09:53

AI 辅助下的移动应用开发毕业设计:从原型到部署的高效实践

毕业设计常见痛点&#xff1a;时间紧、调试难、架构乱 做毕设时&#xff0c;90% 的同学都会踩到同一套坑&#xff1a; 选题宏大&#xff0c;排期却只有 8-10 周&#xff0c;真正留给编码的不足 4 周技术栈不熟&#xff0c;Flutter 与原生通道、Firebase 权限、CI/CD 全是第一…

作者头像 李华
网站建设 2026/10/14 14:32:32

3D图像处理毕设实战:从数据预处理到实时渲染的完整技术链路

3D图像处理毕设实战&#xff1a;从数据预处理到实时渲染的完整技术链路 -- 本科毕设做 3D 图像&#xff0c;最怕“跑不通、跑不快、跑不好看”。这篇笔记把我自己踩过的坑、调通的代码、测出的性能一次性摊开&#xff0c;给你一条能直接抄作业的端到端链路。 一、典型痛点&…

作者头像 李华
网站建设 2026/10/15 10:18:11

超详细版ESP32 Arduino开发环境串口驱动调试日志

ESP32串口连不上&#xff1f;别急着重装驱动——一位嵌入式老兵的“通电即通”调试手记你是不是也经历过&#xff1a;刚拆开一块崭新的ESP32开发板&#xff0c;满怀期待插上USB线&#xff0c;打开Arduino IDE&#xff0c;却在端口列表里看到一片空白&#xff1f;点上传&#xf…

作者头像 李华
网站建设 2026/10/12 3:47:30

LightGBM中early_stopping_rounds参数的正确使用方式与常见报错解析

1. early_stopping_rounds参数的核心作用 当你用LightGBM训练模型时&#xff0c;最怕遇到两种情况&#xff1a;一种是模型训练时间太长浪费资源&#xff0c;另一种是模型在训练集上表现很好但在测试集上表现糟糕。这时候early_stopping_rounds就像个智能管家&#xff0c;能帮你…

作者头像 李华
网站建设 2026/10/10 14:50:02

PostgreSQL核心原理:防止数据丢失的关键操作(真空冻结)

文章目录 一、背景&#xff1a;为什么需要“冻结”&#xff1f;——XID 回卷危机1.1 PostgreSQL 的 MVCC 与 XID1.2 XID 的“环形”特性与回卷问题1.3 解决方案&#xff1a;冻结&#xff08;Freeze&#xff09;机制&#xff08;冻结的本质&#xff09;1.4 更智能的 freeze1.5 真…

作者头像 李华