Java中HashMap的用法详细介绍

2025-08-20 10:50

本文主要是介绍Java中HashMap的用法详细介绍,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

《Java中HashMap的用法详细介绍》JavaHashMap是一种高效的数据结构,用于存储键值对,它是基于哈希表实现的,提供快速的插入、删除和查找操作,:本文主要介绍Java中HashMap...

一.HashMap

1.基本概念

HashMap是基于哈希表实现的Map接口,用于存储键值对(K-V)格式,其核心作用就是通过哈希函数为了更快查询到某个元素,时间复杂度O(1);

2.底层数据结构:

在java(1.7)之前 底层是采用“数组 + 链表” 的形式存储

但在java(1.8) 之后,变成了“数组 + 链表 + 红黑树 ”形式存储

我们主要了解java1.8之后的内容;

Java中HashMap的用法详细介绍

从源码上查看

数组的初始容量为16,负载因子为0.75,所以当超过这个阈值 16 * 0.75 = 12 
设计初始容量为16核心原因是2 的幂次方更适合哈希计算,能减少哈希冲突,但8又太频繁扩容,新增性能开销,而32空间又会太大,则浪费空间~
负载javascript因子也是同理,太小,会频繁扩容,虽然查询快了,但是数组太稀疏,浪费空间,而太大python,就会扩容少,空间利用率高,但查询会变慢~

当插入第13个元素的时候,已经超过阈值,为了避免哈希冲突,会进行扩容~

然后这边当数组大小超过64时候,链表大小超过8的时候,会从链表进化成红黑树,但当元素个数少于6个时,会退回链表~

  1. 链表长度≥8
    基于泊松分布,当负载因子为 0.75 时,链表长度自然增长到 8 的概率极低(约千万分之一),此时说明哈希冲突异常频繁,需要转为红黑树优化。

  2. 数组容量≥64
    若数组容量小于 64(如 32),即使链表长度≥8,也会先触发扩容(而非转红黑树)。原因是:数组容量小时,扩容成本低,通过扩容可分散元素,减少冲突;而红黑树的维护成本(插入 / 删除时的旋转操作)高于扩容,此时扩容更高效。

3.HashCode和equals方法

HashMap中的键一定会实现这俩个方法,hashCode用于计算存储的位置,而equals用于判断来个键是否相同,在put方法的时候,如果俩个哈希值相同,会判断是否是同一个值,如果不是就会放在同一个桶中的不同位置,如果相同就是一个东西~

为什么重写HashCode方法?

如果不重写,这个时候,就会导致俩个相同的key,会被计算出俩个不同的哈希值,导致他们存在在不同的桶中,到时候查询的时候到底是查哪个?

为什么重新equals方法?

equals方法是为了当出现哈希冲突的时候,俩个key的哈希值相同,放在同一桶中,但没有比较它们的对象,可能会误判会导致俩个key存储在不同位置,所以没有覆盖之前的值,就会错误~

class Person {
    String name;
    int age;
    @Override
    public int hashCode() { // 重写hashCode,保证逻辑相等的对象哈希值相同
        return Objects.hash(name, age);
    }
    // 未重写equals()
}

public static void main(String[] args) {
    HashMap<Person, String> map = new HashMap<>();
    Person p1 = new Person("张三", 20);
    Person p2 = new Person("张三", 20);
    map.put(p1, "学生");
    map.put(p2, "工人"); // 哈希值相同,进入同一个桶,但equals判断为不同key
    System.out.println(map.size()); // 仍输出2,而非预期的1
}

hashCode()equals()必须配套重写,且满足以下规则:

  • 一致性:如果两个对象通过equals()判断为相等,则它们的hashCode()必须返回相同的值;
  • 对称性:如果a.equals(b)true,则b.equals(a)也必须为true
  • 非空性:a.equals(null)必须返回false

4.put操作

1.初始化和数组检查

Java中HashMap的用法详细介绍

首先判断 HashMap 是否未初始化(table 数组为 null 或长度为 0),若是则触发 初始化(resize):

2.计算索引并检查桶是否为空

Java中HashMap的用法详细介绍

如果该位置为空,直接创建新节点插入

3.桶不为null,处理哈希冲突

Java中HashMap的用法详细介绍

  1. 检查首节点:若首节点的 key 与传入 key equals 相等(且哈希值相同),则视为 “重复 key”,记录该节点(后续替换其 value)。
  2. 遍历后续节点
    • 若桶是单链表(Node):遍历链表,若找到 key 相等的节点,记录该节点;若遍历到链表尾部仍未找到,则在链尾插入新节点(Node)。
    • 若桶是红黑树(TreeNode):调用红黑树的插入方法(putTreeVal),若找到重复 key 则记录节点,否则插入新树节点。
  3. 处理重复 key:若步骤 1 或 2 中找到重复 key 的节点,用新 value 替换其旧 value,流程结束。

4.判断链表是否转化红黑树

Java中HashMap的用法详细介绍

5.以及数组大小是否超过阈值进行扩容

Java中HashMap的用法详细介绍

5.get操作

同理get 方法的核心逻辑是:

哈希计算→定位桶→根据桶结构(链表 / 红黑树)查找匹配节点

java1.7 -- java1.8 HashMap 做了哪些优化:

哈希值计算:

1.7版本:对 key 的原始哈希值(hashCode() 结果)进行 4 次扰动(多次移位和异或运算)

Java中HashMap的用法详细介绍

1.8版本:简化为 1 次扰动,仅通过一次 “高 16 位与低 16 位异或” 实现混合

Java中HashMap的用法详细介绍

减少计算开销:一次异或运算即可达到近似的混合效果,相比 1.7 的 4 次运算,计算效率更高。

实际效果:在大多数场景下,1 次扰动已能保证哈希值的均匀分布,同时降低了 CPU 运算成本。

链表插入方式:

由头插变成尾插,核心是为了解决多线程扩容时的链表环化问题,同时让链表操作逻辑更合理。

多线程扩容时可能导致链表环化(死循环)

扩容过程中,节点会从旧数组迁移到新数组,头插法在迁移时会反转链表顺序(例如旧链表 A→B,迁移后新链表变为 B→A)。若此时有多个线程同时操作,可能出现节点引用相互指向的情况(如 A.next = B 且 B.next = A),形成环形链表。后续查询时,线程会陷入无限循环,导致 CPU 占用飙升。

二.ConcurrentHashMap

ConcurrentHashMap 是 Java 中用于并发场景的哈希表实现,专为高并发环境设计;

1.Java 1.7 ConcurrentHashMap架构:

Java中HashMap的用法详细介绍

在java1.8之前 ConcurrentHashMap 采用的是 分段数组(Segment)+ 哈希表 , 默认为16个segment,同时支持16个并发~

  • 整体结构:ConcurrentHashMap -> Segment[] -> HashEntry[] -> 链表
  • 写操作(put/remove 等)
    1. 计算 key 的哈希值,定位到对应的 Segment
    2. 获取该 Segment 的锁(若被占用则阻塞)China编程
    3. 在 Segment 内部的哈希表中执行插入 / 删除(类似 HashMap 的逻辑,链表头插法);
    4. 释放锁。
  • 优点:通过分段锁实现了多线程并发写,效率比全表锁(如 Hashtable)高得多。
  • 缺点
    • 锁粒度仍较大(以 Segment 为单位),若多个线程操作同一 Segment,仍会阻塞;
    • 结构复杂China编程,维护成本高;
    • 扩容仅针对单个 Segment,但整体性能受限于 Segment 数量。

2.Java 1.8 ConcurrentHashMap架构:

Java中HashMap的用法详细介绍

摒弃了 Segment 分段结构,底层直接使用 数组 + 链表 + 红黑树(与 JDK 1.8 HashMap 结构类似)

锁的粒度更小,锁的是桶的头节点,并且采取了CAS + synchronized 机制,仅当多个线程操作同一链表 / 红黑树时才会竞争锁,锁冲突概率远低于 1.7 的 Segment 级锁。

CAS+synchronized的使用场景:

  1. 无冲突场景(空桶插入、初始化、低并发计数):用 CAS 实现高效无锁操作。
  2. 有冲突场景(非空桶操作、复杂结构修改):用 synchronized 加锁保证原子性。
版本底层架构哈希表数量锁粒度
JDK 1.7 及之前两层结构:Segment [] 数组 + 每个 Segment 包含一个 HashEntry [] 数组多个(默认 16 个,与 Segment 数量一致)Segment 级(锁整个子哈希表)
JDK 1.8 及之后单层结构:Node [] 数组(链表 / 红黑树解决冲突)1 个(整个 ConcurrentHashMap 共用一个底层数组)节点级(锁链表头节点或红黑树根节点)

扩容区别:

特性JDK 1.7 扩容JDK 1.8 扩容
扩容范围单个 Segment 独立扩容整个数组全表扩容
并发能力单线程扩容(当前操作 Segment 的线程)多线程协同扩容(所有线程可协助迁移)
锁影响范围仅锁定当前 Segment,其他 Segment 可用无全局锁,仅锁定迁移中的桶节点
迁移效率单个 Segment 迁移,效率较低多线程分片迁移,效率更高
节点迁移方式头插法(可能反转链表)尾插法(保持链表顺序,无环化风险)
与读写的兼容性扩容时该 Segment 读写阻塞扩容与读写可并行(读无锁,写锁单个桶)

size()区别:

在JDK 1.7 中,ConcurrentHashMap 由多个 Segment 组成,每个 Segment 独立维护自己的元素数量(count),因此计算总 size 时需要累加所有 Segment 的 count。

  • 为减少误差,JDK 1.7 采用 “重试机制”:如果两次连续累加的结果一致,则认为是准确值;若不一致(说明期间有并发修改),最多重试 3 次,若仍不一致则直接返回当前累加值(接受一定误差)。

而在JDK 1.8中,计算 size 依赖于 baseCount 原子变量和 counterCells 辅助数组,通过无锁累加实现,返回两者的总和作为最终的 size

当插入元素成功后,需要将总数 +1,流程如下:

  1. 优先尝试更新 baseCount
    线程通过 CAS 操作直接更新 baseCountbaseCount + 1)。

    • 若 CAS 成功:计数完成,无需其他操作。
    • 若 CAS 失败:说明存在并发竞争(多个线程同时更新 baseCount),进入下一步。
  2. 竞争激烈时,使用 counterCells 分散计数

    • 若 counterCells 未初始化,先通过 CAS 初始化(创建一个 CounterCell 数组)。
    • 线程通过哈希算法(基于线程 ID 或随机数)随机选择 counterCells 中的一个 CounterCell
    • 尝试用 CAS 更新该 CounterCell 的 valuevalue + 1):
      • 若成功:计数完成。
      • 若失败:重试或选择其他 CounterCell(避免死锁)。
特性JDK 1.7 计数方式JDK 1.8 计数方式
底层依赖各 Segment 的 count 累加baseCount + counterCells 累加
并发处理重试机制减少误差,返回近似值CAS 原子操作,返回接近精确值
性能低并发时高效,依赖 Segment 数量高低并发均高效,通过分散竞争优化
实现复杂度简单(遍历 + 重试)复杂(原子变量 + 辅助数组 + 竞争分散)
适用场景分段锁架构下的近似计数全局数组架构下的高效精确计数

总结 

到此这篇关于Java中HashMap用法详细介绍的文章就介绍到这了,更多相关java hashmap详解内容请搜索China编程(www.chinasem.cn)以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程China编程(www.chinasem.cn)!

这篇关于Java中HashMap的用法详细介绍的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1155825

相关文章

SpringBoot集成XXL-JOB实现任务管理全流程

《SpringBoot集成XXL-JOB实现任务管理全流程》XXL-JOB是一款轻量级分布式任务调度平台,功能丰富、界面简洁、易于扩展,本文介绍如何通过SpringBoot项目,使用RestTempl... 目录一、前言二、项目结构简述三、Maven 依赖四、Controller 代码详解五、Service

Android协程高级用法大全

《Android协程高级用法大全》这篇文章给大家介绍Android协程高级用法大全,本文结合实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友跟随小编一起学习吧... 目录1️⃣ 协程作用域(CoroutineScope)与生命周期绑定Activity/Fragment 中手

Java 正则表达式的使用实战案例

《Java正则表达式的使用实战案例》本文详细介绍了Java正则表达式的使用方法,涵盖语法细节、核心类方法、高级特性及实战案例,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要... 目录一、正则表达式语法详解1. 基础字符匹配2. 字符类([]定义)3. 量词(控制匹配次数)4. 边

Java Scanner类解析与实战教程

《JavaScanner类解析与实战教程》JavaScanner类(java.util包)是文本输入解析工具,支持基本类型和字符串读取,基于Readable接口与正则分隔符实现,适用于控制台、文件输... 目录一、核心设计与工作原理1.底层依赖2.解析机制A.核心逻辑基于分隔符(delimiter)和模式匹

Java中的stream流分组示例详解

《Java中的stream流分组示例详解》Java8StreamAPI以函数式风格处理集合数据,支持分组、统计等操作,可按单/多字段分组,使用String、Map.Entry或Java16record... 目录什么是stream流1、根据某个字段分组2、按多个字段分组(组合分组)1、方法一:使用 Stri

Java+AI驱动实现PDF文件数据提取与解析

《Java+AI驱动实现PDF文件数据提取与解析》本文将和大家分享一套基于AI的体检报告智能评估方案,详细介绍从PDF上传、内容提取到AI分析、数据存储的全流程自动化实现方法,感兴趣的可以了解下... 目录一、核心流程:从上传到评估的完整链路二、第一步:解析 PDF,提取体检报告内容1. 引入依赖2. 封装

使用Spring Cache本地缓存示例代码

《使用SpringCache本地缓存示例代码》缓存是提高应用程序性能的重要手段,通过将频繁访问的数据存储在内存中,可以减少数据库访问次数,从而加速数据读取,:本文主要介绍使用SpringCac... 目录一、Spring Cache简介核心特点:二、基础配置1. 添加依赖2. 启用缓存3. 缓存配置方案方案

Java实现复杂查询优化的7个技巧小结

《Java实现复杂查询优化的7个技巧小结》在Java项目中,复杂查询是开发者面临的“硬骨头”,本文将通过7个实战技巧,结合代码示例和性能对比,手把手教你如何让复杂查询变得优雅,大家可以根据需求进行选择... 目录一、复杂查询的痛点:为何你的代码“又臭又长”1.1冗余变量与中间状态1.2重复查询与性能陷阱1.

深度剖析SpringBoot日志性能提升的原因与解决

《深度剖析SpringBoot日志性能提升的原因与解决》日志记录本该是辅助工具,却为何成了性能瓶颈,SpringBoot如何用代码彻底破解日志导致的高延迟问题,感兴趣的小伙伴可以跟随小编一起学习一下... 目录前言第一章:日志性能陷阱的底层原理1.1 日志级别的“双刃剑”效应1.2 同步日志的“吞吐量杀手”

Spring创建Bean的八种主要方式详解

《Spring创建Bean的八种主要方式详解》Spring(尤其是SpringBoot)提供了多种方式来让容器创建和管理Bean,@Component、@Configuration+@Bean、@En... 目录引言一、Spring 创建 Bean 的 8 种主要方式1. @Component 及其衍生注解