通俗易懂解释Java8 HashMap

我们来用通俗易懂的方式解释一下 Java 8 中 HashMap 的原理,让你对它的结构、运行机制有清晰的理解。


🌳 什么是 HashMap?

HashMap 是 Java 中非常常用的数据结构,用于存储键值对(key-value)。你可以把它理解成一个“大数组”,每个位置可以存储一个或多个数据。


🧱 HashMap 的基本结构

Java 8 中的 HashMap 底层由 数组 + 链表 + 红黑树 组成:

HashMap└── 数组 table[]     ← 每个元素称为“桶”├── 链表/树    ← 存储实际的 key-value 节点(Node)

🛠️ 核心工作流程

我们来一步步看 HashMap 是怎么工作的:


✅ 插入 put(key, value)

举个例子:

map.put("apple", 100);

背后做了这些事:

  1. 计算 hash 值:

    • 对 key(“apple”)调用 hashCode(),然后做了一些扰动处理。
    • 比如 "apple".hashCode() = 93029210,经过扰动后变成一个新的 hash 值。
  2. 计算数组索引:

    • index = hash & (table.length - 1),定位到数组的某一桶。
  3. 判断该桶是否有元素:

    • 如果没有:直接放进去。

    • 如果有:判断 key 是否相同。

      • 相同:替换旧值。
      • 不同:链表/红黑树追加新节点。
  4. 链表转红黑树(Java8 新特性):

    • 当同一个桶内元素数量超过 8 个 且数组长度超过 64,会把链表转成红黑树,提高查询效率。

🔍 查询 get(key)

比如:

map.get("apple");
  1. 计算 hash。

  2. 定位到数组索引。

  3. 在对应的桶里找:

    • 是链表:顺序比较 key。
    • 是红黑树:用树的方式查找。

🔄 发生了哈希冲突怎么办?

多个不同的 key 计算后可能落到同一个数组位置(桶),这就叫 哈希冲突。Java 8 中:

  • 前几个冲突的 key 用 链表 挂在一起;
  • 如果冲突太多(链表长度 > 8 且数组长度 > 64),链表就会转成 红黑树,加快查找速度。

📈 扩容机制

HashMap 的默认容量是 16,装载因子是 0.75,当元素数量 > 16 * 0.75 = 12 时,就会发生 扩容

  1. 扩容为原来容量的 2 倍。
  2. 重新计算所有 key 的 hash 值和位置,迁移元素。

这也是为什么我们建议在使用 HashMap 时 预估容量大小,以避免多次扩容影响性能。


🧠 为什么要用红黑树?

因为链表查找是 O(n),而红黑树是 O(log n),当冲突严重时,查询效率会大幅下降。引入红黑树可以在极端情况下保持较好的性能。


🔥 小结图示:

插入:hash → 定位数组桶 → 空就放,有就比较 → 链表 / 红黑树
查询:hash → 定位 → 链表/树中查找 key
扩容:元素太多 → 数组翻倍 → 元素重新分布

✅ Java 8 中 HashMap 的优点

特性优点说明
引入红黑树避免链表过长导致查找变慢
扰动函数优化减少哈希冲突,分布更均匀
结构清晰数组 + 链表 + 红黑树,灵活扩展

我们来用通俗易懂的语言 + 举例方式,深入讲讲 Java 8 中 HashMap扰动函数(hash 扰动函数),帮助你真正理解它的作用和原理。


一、什么是扰动函数?

✅ 定义一句话:

扰动函数是为了让 hash 值的高位信息也能参与数组下标的计算,从而让 key 更均匀地分布在 HashMap 的数组中。


二、为什么需要扰动函数?

🚨 问题:低位碰撞严重!

在 HashMap 中,定位数组下标的方式是这样的:

index = hash(key) & (table.length - 1)

由于 table.length 是 2 的幂,这样的 & 操作会只保留 hash 值的低位

➡️ 如果两个 key 的 hash 值只有高位不同,而低位相同,它们就会落入同一个桶(发生冲突)。

🧠 举个例子:

hash1 = 0b10110000_00000000_00000000_00000001
hash2 = 0b01010000_00000000_00000000_00000001

虽然高位差很多,但最后的几位一样。由于我们只用低位参与数组下标计算,这两个 key 会落在同一个位置!

✅ 解决方案:扰动函数

通过扰动函数把 高位信息混合到低位,避免冲突更均匀。


三、Java 8 中的扰动函数源码分析

HashMap 中源码如下(简化):

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

🚀 解释:

  • h = key.hashCode():拿到 key 的原始 hashCode(32 位整数)。
  • h >>> 16:无符号右移 16 位,把高位移动到低位。
  • ^:做异或运算,把原来的高位和低位混合起来,让高位也参与到最终结果中

四、通俗举例讲解

假设我们有两个 key:

key1.hashCode() = 0b10110000_00000000_00000000_00000001
key2.hashCode() = 0b01010000_00000000_00000000_00000001

它们的低位是一样的,都会落到相同数组位置,发生 hash 冲突。

有了扰动函数:

扰动结果 = h ^ (h >>> 16)

扰动后:

  • key1 的高位会被移到低位并参与混合;
  • 即便低位一样,只要高位不同,扰动后结果就不一样。

这样就打乱了 hashCode 的原始规律,使得分布更随机,降低 hash 冲突概率。


五、总结(口诀记忆)

  • 🚩 低位计算索引,高位白白浪费。
  • 🧠 扰动函数混高低,分布均匀更合理。
  • 异或右移搞起来,冲突少,效率高。

六、动手试一试(简单代码示例)

public class HashDisturbExample {public static void main(String[] args) {String key = "apple";int h = key.hashCode();int disturbed = h ^ (h >>> 16);System.out.println("原始 hashCode: " + h);System.out.println("扰动后结果:     " + disturbed);}
}

运行后你会看到两个不同的值,这说明扰动函数确实改变了 hash 值结构,帮助分布更均匀。


✅ 第一个问题:为什么 table.length 是 2 的幂?

🔧 原因:

为了 优化取模(mod)操作为位运算(&)操作,提升性能!

🧠 解释:

HashMap 中,我们要根据 key 的 hash 值决定存放的位置,也就是数组下标:

index = hash(key) % table.length

如果 table.length 是任意数字,比如 10,那就必须使用 除法运算 %,这是一个比较慢的操作。

而如果 table.length 是 2 的幂,比如 16、32、64,我们就可以写成:

index = hash(key) & (table.length - 1)

⚠️ 这行代码的效果和 % 是一样的,但效率更高

🧠 举例说明:

假设 table.length = 16(即 2⁴),那么 16 的二进制是:

16 = 10000(即 2 的 4 次方)
16 - 1 = 15 = 01111(二进制)

这样做 & 15 相当于取 hash 值的 低 4 位,效果等同于 hash % 16,而且更快!


✅ 第二个问题:为什么 & 的操作和 % 是一样的?

🎯 只有在 table.length2 的幂时,才成立!

hash % 16 == hash & (16 - 1)

这是因为 2 的幂次减 1 得到的是全 1 的二进制位数。比如:

table.length二进制table.length - 1二进制
8100070111
16100001501111
3210000031011111

当你对一个数 x 执行 x & (n - 1),就等于 取 x 的低几位,这等价于 x % n(当 n 是 2 的幂时)。

✅ 举个例子:

hash = 0b101010        // 十进制 42
table.length = 16      // 2⁴
mask = 16 - 1 = 15 = 0b111142 % 16 = 10
42 & 15 = 0b101010 & 0b1111 = 0b1010 = 10

结果一样,但是 & 运算效率远高于 % 运算。


✅ 第三个问题:为什么是“低位”参与运算?

📌 因为 hash & (table.length - 1) 只保留 低位信息

你刚刚也看到了,举个例子:

hash = 0b10110000_00000000_00000000_00001101
table.length = 16 (2),mask = 0b00000000_00000000_00000000_00001111

执行 &

hash & mask =
0b10110000_00000000_00000000_00001101
&
0b00000000_00000000_00000000_00001111
=
0b00000000_00000000_00000000_00001101 => 13

➡️ 所以只有 低 4 位被保留下来,高位被舍弃了!


🤔 那高位的 hash 值不是白计算了吗?

是的,如果不做处理,高位信息就浪费了,这就是为啥我们需要:

🚀 扰动函数:h ^ (h >>> 16)

它会把高 16 位的信息也混入低 16 位中,确保高位不会被浪费。


✅ 总结归纳:

问题解答
为什么 table.length 是 2 的幂?为了将取模 % 优化成更快的 & 位运算。
为什么 & 可以代替 %当除数是 2 的幂时,x & (n - 1) 等价于 x % n,而效率更高。
为什么是低位参与?因为 & 操作只保留了低位的信息(如 & 15 保留低 4 位),高位信息被忽略。

🧠 为什么要扩容?

HashMap 是通过数组 + 链表(或红黑树)来存储键值对的。它的性能依赖于:

键值对在数组中的分布是否均匀(冲突少)

如果 HashMap 存放的元素太多,冲突越来越多,链表变长,性能会急剧下降,所以需要:

扩容(resize):将原数组变大,把原来的元素“重新分布”到新的数组中。


⚙️ HashMap 扩容时机

HashMap 会在满足以下条件时扩容:

当前元素个数 > 阈值(threshold) = 容量 × 负载因子(loadFactor)
  • 默认初始容量是 16
  • 默认负载因子是 0.75
  • 所以默认第一次扩容是在元素数量超过 16 × 0.75 = 12 时发生

🚀 扩容做了什么?

当发生扩容时,HashMap 会做几件事:

  1. 将数组容量扩大为 原来的两倍
  2. 创建一个新的数组(新的 table)
  3. 重新计算每个键值对的索引(index)
  4. 将所有旧元素 “搬家” 到新数组中

📊 举个例子来说明

假设你有一个容量为 4 的 HashMap,插入了 3 个元素(已接近 0.75 的阈值):

原数组(长度4):
index 0: [A -> 1]
index 1: [B -> 2]
index 2: [C -> 3]
index 3: null

你再插入一个元素 D -> 4,此时元素数目为 4,超过 4 × 0.75 = 3,触发扩容。

扩容过程:

  • 数组变为长度 8
  • 所有键的 hash 会重新计算 index
  • 每个键值对会根据新的 index 放到新数组中,分布更均匀
扩容后数组(长度8):
index 0: null
index 1: [A -> 1]
index 2: null
index 3: [C -> 3]
index 4: null
index 5: [B -> 2]
index 6: [D -> 4]
index 7: null

这样就避免了原来在小数组中“拥挤”的问题。


🔁 元素“搬家”的优化:位运算重分布

Java 8 做了一个优化,在扩容时 不再重新计算 hash 值,而是通过如下逻辑判断元素是留在原位置还是移动到“新位置”:

// 假设 oldCap 是原数组长度,newCap = oldCap * 2
if ((e.hash & oldCap) == 0) {// 留在原地
} else {// 移动到原位置 + oldCap
}

这个技巧利用了 2 的幂次 的特性,通过 hash & oldCap 直接判断是否需要挪动,提高了效率!


💡 为什么扩容不是一开始就做得很大?

为了节省内存。HashMap 的容量和负载因子设计是为了在性能和内存之间做权衡。


✅ 总结:HashMap 扩容机制

步骤内容
扩容时机元素个数 > 阈值(容量 × 负载因子)
扩容操作数组变为原来的两倍
元素处理每个元素根据新数组重新定位(高效位运算)
优化手段hash & oldCap 判断是否搬家
目的降低 hash 冲突,提高查询性能

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/pingmian/91546.shtml
繁体地址,请注明出处:http://hk.pswp.cn/pingmian/91546.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

macOS安装配置Unbound DNS完整指南

文章目录macOS安装配置Unbound DNS完整指南🎯 为什么选择Unbound?📋 系统要求🚀 安装步骤1. 使用Homebrew安装2. 查看安装信息⚙️ 基础配置1. 备份默认配置2. 创建基础配置文件3. 基础配置内容配置53端口版本(高级用户…

学习模板元编程(2)std::true_type/false_type

目录 实现原理 应用场景 条件编译 通过特化和继承,实现std::is_xxx系列 思路 举例 例子1,is_bool 例子2,is_ptr 实现原理 std::true_type/false_type是模板intergral_constant的两种实现: using true_type integral_co…

Chain-of-Thought Prompting Elicits Reasoning in Large Language Models论文阅读笔记

Chain-of-Thought Prompting Elicits Reasoning in Large Language Models 摘要 本文探索了思维链(chain of thought),即一系列中间推理过程,可以有效地增强大语言模型的复杂推理能力。 在三个大型语言模型上的实验表明&#xff0…

华为核心交换机S7700的内存OID

华为S7700系列交换机 SNMP内存相关OID说明 以下列出了华为S7700核心交换机在SNMP v2c下可用的内存相关OID,包括CPU内存利用率、物理内存总量、已用内存和空闲内存,并给出每个OID的功能描述、数据类型、单位、使用说明等信息。 1. CPU内存利用率(处理器内存占用百分比) OID名…

中州养老Day02:服务管理护理计划模块

本日任务:服务管理的后端开发 1.学习:护理项目 (1)评估开发工期的思路和注意事项 全面熟悉项目,了解项目重点,设置开发优先级 比如,在下面图片的接口文档中版本有1.0,2.0,3.0也就是功能的初代,二代,三代,所以我们在大致浏览所有功能后,要优先关注初代功能的实现 开发计划 …

JavaScript:Ajax(异步通信技术)

一、Ajax 核心概念Ajax(Asynchronous JavaScript and XML)是一种异步通信技术,核心特点:无刷新更新:无需重新加载整个页面异步处理:后台发送/接收数据不阻塞用户数据格式:支持 XML/JSON/HTML/纯…

leetcode 118. 杨辉三角 简单

给定一个非负整数 numRows&#xff0c;生成「杨辉三角」的前 numRows 行。在「杨辉三角」中&#xff0c;每个数是它左上方和右上方的数的和。示例 1:输入: numRows 5 输出: [[1],[1,1],[1,2,1],[1,3,3,1],[1,4,6,4,1]]示例 2:输入: numRows 1 输出: [[1]]提示:1 < numRows…

jmeter--While控制器--循环直到接口响应符合条件

场景描述业务场景&#xff1a;单据计算接口情况&#xff1a;单据计算&#xff0c;调用接口1发起计算&#xff0c;接口2查询计算执行结果jmeter脚本&#xff1a;把接口1和接口2&#xff08;接口2循环调用&#xff0c;直到返回执行完成状态&#xff09;添加到一个事务&#xff0c…

组播 | 不同 VLAN 间数据转发实现逻辑 / 实验

注&#xff1a;本文为 “不同 vlan 间组播数据转发” 相关合辑。 图片清晰度受引文原图所限。 略作重排&#xff0c;如有内容异常&#xff0c;请看原文。 组播 VLAN&#xff1a;解决路由器为不同 VLAN 用户复制多份流量问题 aiaiai010101 于 2018-11-16 22:42:06 发布 一、组…

渗透测试常用指令

互联网设备的开放信息查询网站&#xff1a; https://fofa.info/ https://www.zoomeye.org/ https://quake.360.net/quake/#/index https://x.threatbook.com/v5/mapping https://hunter.qianxin.com/ 目录 一、网络探测与扫描 traceroute whatweb ping fping nc n…

51单片机串行通信的设计原理有哪些?

51单片机是指由美国INTEL公司生产的一系列单片机的总称&#xff0c;这一系列单片机包括了许多品种&#xff0c;如8031&#xff0c;8051&#xff0c;8751&#xff0c;8032&#xff0c;8052&#xff0c;8752等&#xff0c;其中8051是最早最典型的产品&#xff0c;该系列其它单片机…

设计模式十四:适配器模式(Adapter Pattern)

适配器模式&#xff08;Adapter Pattern&#xff09;是一种结构型设计模式&#xff0c;用于将一个类的接口转换成客户端期望的另一个接口&#xff0c;使原本不兼容的类可以一起工作。适配器模式的类型类适配器&#xff08;通过多重继承实现&#xff09;对象适配器&#xff08;通…

力扣经典算法篇-38-组合(回溯算法)

1、题干 给定两个整数 n 和 k&#xff0c;返回范围 [1, n] 中所有可能的 k 个数的组合。 你可以按 任何顺序 返回答案。 示例 1&#xff1a; 输入&#xff1a;n 4, k 2 输出&#xff1a; [ [2,4], [3,4], [2,3], [1,2], [1,3], [1,4], ] 示例 2&#xff1a; 输入&#xff1a;…

多人命题系统

目 录 摘 要 Abstract 1 系统概述 1.1 概述 1.2课题意义 1.3 主要内容 2 系统开发环境 2. 1 JAVA简介 2. .2 B/S架构 2.3 SSM三大框架 2.4访问数据库实现方法 2.5 系统对MySQL数据库的两种连接方式 3 需求分析 3.1技术可行性&#xff1a;技术背景…

UDP_千兆光通信(四)Tri Mode Ethernet MAC ip核

Tri Mode Ethernet MAC ip核使用与例程分析 一、 Tri Mode Ethernet MAC ip核功能 二、 Tri Mode Ethernet MAC ip核配置 数据传输速率 主要设置接口 帧滤波功能选择,以及流控选择 三、 Tri Mode Ethernet MAC ip核使用 3.1 ip核接口 3.2 ip核接口说明 3.2.1 tx_ifg_delay 3.2…

Linux网络:多路转接 epoll

Linux网络&#xff1a;多路转接 epoll一、epoll三个接口函数1、epoll_create2、epoll_ctl3、epoll_wait二、epoll的工作原理三、epoll的echo_server1、EpollServer类2、构造函数3、事件循环4、事件派发5、事件处理6、测试四、LT和ET模式1、LT2、ET五、项目代码一、epoll三个接口…

uniapp 微信小程序 列表点击分享 不同的信息

<button open-type"share" plain class"item share" click.stop"shareFn(item)"><text>分享</text> </button>import {onShareAppMessage} from dcloudio/uni-applet shareObj ref({})// 将点击后的分享设置信息 关键…

C# 匿名方法详解

C# 匿名方法详解 引言 在C#编程语言中,匿名方法是使用Lambda表达式创建的没有名称的方法。它们在LINQ查询、事件处理和其他场合中非常有用。本文将详细介绍C#匿名方法的基本概念、语法、使用场景以及优势。 匿名方法的概念 匿名方法是一种无需显式定义名称的方法。在C#中,…

SD卡简介与驱动开发

基本概念 存储卡有很多种类&#xff0c;CF卡、记忆棒、SD卡、XD卡、MMC卡、MS卡、TF卡、MicroSD卡等。平时最常见的有SD卡和MicroSD卡两种&#xff0c; SD卡和MicroSD只是两张卡的大小不同&#xff0c;规格版本是完全相同的&#xff0c;均由SD卡协会推出。 SD卡有不少规范&…

大数据平台数仓数湖hive之拉链表高效实现

对于缓慢变化的维度表&#xff0c;如客户表&#xff0c;员工表&#xff0c;为了不丢失历史数据&#xff0c;又不至于太浪费存储空间&#xff0c;我们采用拉链表实现。 实现过程如下&#xff1a; 1、采集初始数据&#xff1a; 1.1 从mysql导出数据到hdfs /data/dolphinschedu…