HashMap的理解与结构

HashMap的基本概念

HashMap是 Java 中最常用的数据结构之一,属于 java.util 包,实现了 Map<K, V> 接口,用于存储 键值对(Key-Value Pair)。它的核心特点是:通过键(Key)快速查找对应的值(Value),平均时间复杂度为 O(1)。

HashMap的内部结构

众所周知,HashMap底层结构,1.8之前是数组+链表,从1.8开始则是数组+链表+红黑树(链表长度大于8),线程不安全。

数组+链表

水平线上,每个 key 经过 hash 计算后得到一个数组下标 index,这个 index 就代表 一个桶(bucket)。

所有 hash 后落在 同一个 index 的 key-value 对,都会被组织在这个桶里 ------ 初始是链表,冲突多时可能转为红黑树。

当 这个桶里的节点数 ≥ 8,并且整个 HashMap 底层数组的长度(即桶的总数)≥ 64 时,才会将该链表 转换为红黑树

同一个 index 是一个桶,桶内链表 ≥8 且总桶数 ≥64 才转红黑树

某个桶(bucket)中的链表长度 ≥ 8

→ 即:有 8 个或更多 key 经过 hash 计算后落在同一个 index,形成链表。

HashMap 底层数组的长度(桶的总数)≥ 64

→ 即:table.length >= MIN_TREEIFY_CAPACITY(默认 64)

  • 💡 举个形象比喻:

想象一条 64 个车位的停车场(水平数组):

  • 每个车位是一个 桶(bucket)
  • 车牌号(key)经过某种规则(hash)分配到某个车位
  • 如果多个车被分到同一个车位(hash 冲突),就 垂直叠起来停(链表)
  • 当某个车位叠了 8 辆车以上,且 整个停车场 ≥64 个车位 → 管理员就把这个车位改成 立体车库(红黑树),方便快速找车
  • 但如果停车场只有 32 个车位,哪怕某个车位堆了 10 辆车 → 管理员会说:"车位太少才导致拥堵,先扩建停车场!"(扩容),而不是建立体车库

哈希冲突

hash冲突:HashMap中主要是通过key的hashCode来计算hash值的,只要hashCode相同,计算出来的hash值就一样。如果存储的对象对多了,就有可能不同的对象所算出来的hash值是相同的,这就出现了所谓的hash冲突。详情如下:

随着存储的对象数量不断增加,可能会导致以下几种情况:

1.存储对象key 值相同(hash值一定相同),导致冲突;

2.存储对象key 值不同,由于 hash 函数的局限性导致hash 值相同,冲突;

3.存储对象key 值不同,hash 值不同,但 hash 值对数组长度取模后相同,冲突;

HashMap里面解决hash冲突的方法就是链地址法,

链地址法的基本思想是:每个哈希表节点都有一个next指针,多个哈希表节点可以用next指针构成一个单向链表,被分配到同一个索引上的多个节点可以用这个单向 链表连接起来,如: 键值对k2, v2与键值对k1, v1通过计算后的索引值都为2,这时及产生冲突,但是可以通道next指针将k2, k1所在的节点连接起来,这样就解决了哈希的冲突问题 。

扩容问题

List 的继承关系图

实线代表继承 虚线代表实现接口

List 的特性比较

特性 ArrayList LinkedList Vector
底层结构 动态对象数组 Object[] elementData 双向链表 Node<E>(包含前驱、后继指针) 动态对象数组 Object[] elementData
线程安全 非线程安全(多线程并发读写可能抛出 ConcurrentModificationException 非线程安全(同上) 线程安全 (关键方法如 add()get() 使用 synchronized 修饰)
扩容机制 - 触发条件:添加元素时容量等于元素个数 - 默认初始容量:10(空参构造) - 扩容规则:newCapacity = oldCapacity + (oldCapacity >> 1)(即 1.5 倍) - 无负载因子概念 不涉及传统扩容。新增元素时动态创建节点,仅调整前后指针。内存开销包含元素本身 + 两个指针 - 触发条件:添加元素时容量等于元素个数 - 默认初始容量:10 - 扩容规则:若未设置 capacityIncrement,则 newCapacity = oldCapacity * 2(2 倍);否则按增量扩展 - 无负载因子概念
核心对比点 - 随机访问快 :基于索引,时间复杂度 O(1) - 尾部增删快 :接近 O(1) - 中间增删慢 :需移动后续元素,O(n) - 内存紧凑:仅存对象,无额外指针 - 随机访问慢 :需从头/尾遍历,平均 O(n) - 任意位置增删快 (已知节点):仅改指针,O(1);但查找位置仍需 O(n) - 内存开销大 :每个节点含两个指针 - 实现 Deque 接口:可作双端队列使用 - 历史遗留类 :JDK 1.0 引入,设计较陈旧 - 同步开销大 :方法级 synchronized 导致高并发性能差 - 现代替代方案 :推荐使用 CopyOnWriteArrayListCollections.synchronizedList(new ArrayList<>())

HashMap的扩容

HashMap、Hashtable、ConcurrentHashMap

特性 HashMap Hashtable ConcurrentHashMap
线程安全 ❌ 非线程安全 ✅ 线程安全(方法级 synchronized ✅ 线程安全(JDK 8+:synchronized + CAS;JDK 7:分段锁)
性能 🚀 高(无锁开销) 🐌 低(全表加锁,高并发下严重阻塞) ⚡ 高(读操作几乎无锁,写操作锁粒度细)
Key/Value 空值 ✅ 允许 1 个 null ,多个 null 不允许 null 键或值 (会抛 NullPointerException 不允许 null 键或值(避免并发语义歧义)
继承体系 继承 AbstractMap 继承 Dictionary(已过时的抽象类) 继承 AbstractMap,实现 ConcurrentMap 接口
默认初始容量 16 11 16
扩容负载因子 0.75(默认) 0.75(默认) 0.75(默认)
扩容机制 2 倍扩容(newCap = oldCap << 1 2 倍 +1 扩容(newCap = oldCap * 2 + 1 分段并发扩容,多个线程可协助迁移数据(JDK 8+)
负载因子原因 0.75 是空间与时间的最佳权衡: • 过低(如 0.5)→ 内存浪费 • 过高(如 0.9)→ 哈希冲突激增 同 HashMap,但因初始容量为 11(质数),可更好分散哈希冲突 同 HashMap,但并发环境下扩容逻辑更复杂,需保证线程安全
遍历方式 Iteratorfail-fast:并发修改会抛异常) Enumeration(古老接口,非 fail-fast Iterator弱一致性:不抛并发异常,可能看不到最新修改)
并发机制 全表锁(每个 public 方法加 synchronized JDK 7:分段锁(Segment) JDK 8+:synchronized 锁桶头 + CAS + 红黑树
并发度 不适用 1(整张表一把锁) 默认 16(JDK 7 可配置;JDK 8+ 动态调整,最大 65535)
可代替度 ✅ 单线程场景的标准选择 ⚠️ 已过时,不应在新项目中使用 Hashtable 的现代替代者,高并发首选
应用场景 • 单线程缓存(如 Spring 单例 Bean 缓存) • 系统配置参数存储 • 单体应用中的 Session 属性管理 • 仅存在于老旧遗留系统 中 • 新项目应避免使用 • 高并发缓存(如电商商品库存) • 实时计数器(UV/PV 统计) • 分布式会话共享(配合 Redis 等)

红黑树 (自平衡二叉搜索)

结构特点

  • 每个节点非红即黑
  • 根节点必须是黑色
  • 红色节点的子节点必须是黑色(不能连续红)
  • 从任一节点到其所有叶子的路径,黑色节点数相同 --->(红黑树的"叶子" = 黑色 NIL 节点)

为什么用它?

插入/删除时通过 旋转 + 变色 保持平衡 调整的次数相对较少,通常最多需要 3 次旋转

保证树高 ≤ 2 log₂(n+1) → 操作稳定在 O(log n)

红黑树的操作

添加(找到合适图,我再加上去)

先查找,确定插入位置(原理同二叉排序树)插入新节点

  • 如果新节点是根,则染为Blank(黑色)
  • 如果新节点非根,则染为Red(红色)
  • 如果插入新节点,满足红黑树定义,则插入结束
  • 如果插入新节点,不满足红黑树定义,则需要调整(调整的依据:看新节点叔叔的脸色)
1、黑叔 旋转 + 染色

旋转类型 LL、RR、LR、RL

关键就看 你(N)、你爸(P)、你爷(G)三人的站位关系:

你和你爸的位置 旋转类型 口诀关键词 染色(只限插入)
你和你爸都在爷爷 左边 LL "左左" 父升爷,爷降兄,新根染黑两娃红
你和你爸都在爷爷 右边 RR "右右" 父升爷,爷降兄,新根染黑两娃红
你在爷爷 左边,但你是你爸的 右娃 LR "左右" 左旋爸,右旋爷,你上C位穿黑靴,左爸右爷穿红鞋
你在爷爷 右边,但你是你爸的 左娃 RL "右左" 左旋爸,右旋爷,你上C位穿黑靴,左爷右爸穿红鞋

先看 你爸在爷爷哪边?(左 or 右)

再看 你在你爸哪边?(左 or 右)

拼起来就是类型:(爸的位置)+(你的位置)

2、红叔 染色 + 变新(叔、父、爷 染色,爷变新节点 )

顺口溜帮助牢记:父叔红,黑黑红,上冒根黑终告成

父叔红 → 爸爸和叔叔都是红色(违规条件)
黑黑红 → 爸爸变黑、叔叔变黑、爷爷变红(核心变色)
上冒根黑成 → 向上冒泡修复,直到根为黑色,大功告成!

删除

后面再补,太复杂了,耽误时间。

相关推荐
小猪快跑爱摄影2 小时前
【AutoCad 2025】【C#】零基础教程(二)——遍历 Entity 插件 =》 AutoCAD 核心对象层级结构
开发语言·c#·autocad
hhzz2 小时前
Spring Boot整合Activiti的项目中实现抄送功能
java·spring boot·后端
Dxy12393102162 小时前
Python字符串处理全攻略
开发语言·python
初心灬2 小时前
Java 对接coze工作流
java
代衡_Monster3 小时前
通过位运算实现Java逻辑的包含关系
java·java-ee
毕设源码-朱学姐3 小时前
【开题答辩全过程】以 基于Java的失物招领系统设计与实现为例,包含答辩的问题和答案
java·开发语言
Gomiko3 小时前
JavaScript进阶(四):DOM监听
开发语言·javascript·ecmascript
清晓粼溪3 小时前
统一异常处理
java·开发语言
TH_13 小时前
4、前台界面,表格列名写错
java