HashMap基础

一、核心特性

特性 说明
存储结构 JDK7:数组 + 链表;JDK8:数组 + 链表/红黑树(链表过长时优化)
键值规则 允许 null 键(仅1个,因为键唯一)和 null 值(多个)
有序性 无序(插入顺序 ≠ 遍历顺序,底层哈希分布决定)
线程安全 非线程安全(多线程操作易出现数据丢失、死循环、快速失败等问题)
哈希冲突解决 链地址法(拉链法),JDK8 引入红黑树优化查询效率
快速失败(fail-fast) 遍历过程中修改集合会抛出 ConcurrentModificationException
初始容量/负载因子 默认初始容量16(2的幂),默认负载因子0.75,扩容阈值=容量×负载因子

二、底层实现(JDK7 vs JDK8 核心区别)

1. 存储结构差异

版本 核心结构 节点类型 插入方式 核心优化点
JDK7 数组(Entry[] table)+ 链表 Entry(key/value/hash/next) 头插法 多次哈希扰动保证分布均匀
JDK8 数组(Node[] table)+ 链表/红黑树 Node(基础节点)/TreeNode(红黑树节点) 尾插法 红黑树优化链表查询、简化哈希算法

2. 关键概念解释

  • 数组(哈希桶):HashMap 的底层数组称为「哈希桶」,每个桶对应一个索引,存储链表/红黑树的头节点。
  • 链表:解决哈希冲突(不同 key 计算出相同索引),冲突的节点链接成链表。
  • 红黑树 :JDK8 新增,当链表长度 ≥ 8 数组长度 ≥ 64 时,链表转为红黑树(查询效率从 O(n) 优化为 O(logn));当红黑树节点数 ≤ 6 时,转回链表(避免红黑树维护成本)。

三、核心参数与哈希算法

1. 核心参数

参数名 默认值 作用说明
initialCapacity 16 初始容量(必须是2的幂,默认16),哈希桶的初始长度
loadFactor 0.75 负载因子:衡量数组填充程度的阈值,平衡「时间/空间」(0.75是统计最优值)
threshold 12(16×0.75) 扩容阈值:当元素数量 ≥ threshold 时,触发数组扩容
MAXIMUM_CAPACITY 2^30 最大容量(避免数组过大导致内存溢出)
modCount - 修改次数:用于快速失败机制,记录集合的增/删操作次数

2. 为什么初始容量是 2 的幂?

HashMap 中,key 的索引计算公式为:索引 = hash(key) & (容量 - 1)

  • 若容量是 2 的幂,容量 - 1 的二进制是全 1(如 16-1=15 → 1111),此时 & 运算等价于「取模运算」,但位运算效率远高于取模;
  • 全 1 的二进制能让 hash 值的每一位都参与运算,减少哈希冲突,让节点分布更均匀。

3. 哈希算法(JDK7 vs JDK8)

JDK7:多次扰动(4次位运算 + 5次异或)
java 复制代码
final int hash(Object k) {
    int h = hashSeed;
    if (0 != h && k instanceof String) {
        return sun.misc.Hashing.stringHash32((String) k);
    }
    h ^= k.hashCode();
    h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);
}
  • 目的:通过多次位运算打乱 hash 值的二进制,减少冲突。
JDK8:简化扰动(高16位 ^ 低16位)
java 复制代码
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
  • 优化:保留高16位参与运算(低16位和高16位异或),既保证随机性,又降低计算成本;
  • 最终索引:hash & (capacity - 1)

四、核心方法流程

1. put 方法(JDK8 核心流程)

HashMap 存储键值对的核心逻辑,步骤如下:

非空

非空
匹配
不匹配


找到
未找到
是且数组长度≥64



检查数组是否为空
调用resize()初始化/扩容
计算key的hash值,得到索引i
检查table[i]是否为空
创建Node节点放入table[i]
判断首节点是否匹配key(hash相等+equals相等)
替换value并返回旧值
判断是否是红黑树节点
调用putTreeVal()插入红黑树
遍历链表,查找匹配key
尾插法插入链表尾部
检查链表长度≥8?
链表转红黑树
不转换
size++,检查是否超过threshold
调用resize()扩容
结束

关键细节

  • JDK8 改用「尾插法」:解决 JDK7 头插法在并发扩容时导致的「循环链表」问题;
  • 红黑树转换条件:链表长度 ≥8 数组长度 ≥64(若数组长度<64,先扩容而非转红黑树)。

2. get 方法(JDK8 核心流程)

根据 key 获取 value 的核心逻辑:

  1. 检查数组非空且目标索引有节点;
  2. 检查首节点是否匹配 key(hash+equals),匹配则返回 value;
  3. 不匹配则判断节点类型:
    • 红黑树:调用 getTreeNode() 按红黑树规则查找;
    • 链表:遍历链表查找,匹配则返回 value;
  4. 未找到则返回 null

3. resize 方法(扩容流程)

当元素数量 ≥ threshold 时触发扩容,核心步骤:

  1. 计算新容量 :旧容量翻倍(oldCap << 1),最大不超过 2^30;
  2. 计算新阈值 :旧阈值翻倍(oldThr << 1);
  3. 转移节点 :创建新数组,将旧数组节点迁移到新数组:
    • 链表节点:新索引 = hash & (newCap - 1),结果只有两种可能:原索引 或 原索引 + 旧容量;
    • 红黑树节点:拆分红黑树,若拆分后节点数 ≤6,转回链表;
  4. 替换旧数组为新数组,完成扩容。

五、哈希冲突解决

HashMap 采用「链地址法(拉链法)」解决哈希冲突:

  • 原理:数组的每个索引对应一个链表/红黑树,哈希值相同的 key 被放入同一个链表/红黑树中;
  • 对比其他冲突解决方式:
    • 开放定址法:容易产生堆积,查询效率低;
    • 再哈希法:计算成本高;
    • 链地址法:无堆积、容错性高,是 HashMap 的最优选择。

六、线程安全问题

1. 非线程安全的表现

  • JDK7 并发扩容:头插法导致链表循环,get 操作时死循环;
  • 多线程 put:可能覆盖彼此的操作,导致数据丢失;
  • 快速失败:遍历过程中修改集合,抛出 ConcurrentModificationException

2. 线程安全替代方案

方案 实现原理 效率 推荐度
Hashtable 方法加 synchronized 低(全局锁) ⭐⭐
Collections.synchronizedMap 包装类,底层 synchronized 中(全局锁) ⭐⭐⭐
ConcurrentHashMap JDK7:分段锁;JDK8:CAS + synchronized 高(局部锁) ⭐⭐⭐⭐⭐

七、快速失败(fail-fast)机制

1. 原理

HashMap 内部维护 modCount(修改次数),迭代器遍历前会记录 expectedModCount = modCount,遍历过程中每次操作都会检查 modCount == expectedModCount

  • 相等:正常遍历;
  • 不相等:抛出 ConcurrentModificationException

2. 触发场景

  • 遍历(迭代器/增强 for)时调用 put()/remove()(迭代器自身的 remove() 除外);
  • 多线程同时修改集合。

3. 注意

fail-fast 是「检测机制」,不保证 100% 触发(如修改操作未及时更新 modCount),仅用于暴露非线程安全问题。

八、关键注意点

1. 自定义类作为 Key 的要求

若自定义类作为 HashMap 的 Key,必须重写 hashCode()equals()

  • 重写 equals():保证逻辑上相等的对象被判定为相等;
  • 重写 hashCode():保证 equals() 相等的对象,hashCode() 一定相等(否则会被当成不同 Key 存储)。

2. 不可变类适合作为 Key

推荐使用 StringInteger 等不可变类作为 Key:

  • 不可变类的 hashCode() 计算后不会变化,避免 Key 的 hash 值改变后无法查找;
  • 若 Key 是可变类(如自定义对象),修改其属性会导致 hashCode() 变化,进而无法通过原 Key 获取 Value。

3. 负载因子的调整

  • 负载因子调大:数组填充更满,空间利用率高,但哈希冲突概率增加,查询变慢;
  • 负载因子调小:数组填充更空,查询更快,但空间浪费多;
  • 场景适配:若内存充足、追求查询效率,可设为 0.5;若内存紧张、追求空间利用率,可设为 0.8(不建议超过0.85)。

九、常见面试高频问题

  1. HashMap 底层实现?(分 JDK7/8 讲数组+链表/红黑树,重点讲 JDK8 优化);
  2. 为什么初始容量是 2 的幂?(位运算效率高、哈希分布均匀);
  3. 负载因子为什么是 0.75?(时间/空间平衡,泊松分布统计得出冲突概率最低);
  4. JDK8 为什么引入红黑树?(链表查询 O(n),红黑树 O(logn),优化长链表查询);
  5. HashMap 和 ConcurrentHashMap 的区别?(线程安全、实现原理、性能);
  6. 为什么重写 equals 必须重写 hashCode?(保证相等对象的 hash 值一致,避免哈希冲突);
  7. JDK7 和 JDK8 HashMap 的核心区别?(结构、插入方式、哈希算法、扩容逻辑)。

十、核心区别总结(JDK7 vs JDK8)

维度 JDK7 JDK8
存储结构 数组 + 链表 数组 + 链表/红黑树
插入方式 头插法 尾插法
哈希算法 多次扰动(复杂) 高16位^低16位(简化)
扩容节点转移 重新计算 hash,头插 无需重算 hash,按索引迁移
并发问题 循环链表(死循环) 无循环链表,但仍非线程安全
查询效率 O(n)(链表) O(logn)(红黑树)
相关推荐
期待のcode2 小时前
Java String类
java·开发语言
资生算法程序员_畅想家_剑魔2 小时前
Java常见技术分享-17-多线程安全-并发编程的核心问题的解决方案
java·开发语言
p&f°2 小时前
垃圾回收两种算法
java·jvm·算法
myq992 小时前
第三章:Java异常处理
java·开发语言·笔记
靠沿2 小时前
Java数据结构初阶——堆与PriorityQueue
java·开发语言·数据结构
禾叙_2 小时前
HashMap
java·数据结构·哈希算法
SadSunset2 小时前
(44)Spring6集成MyBatis3.5(了解即可,大部分用springboot)
java·spring boot·后端
LYOBOYI1232 小时前
qt的事件传播机制
java·前端·qt
短剑重铸之日2 小时前
《深入解析JVM》第四章:JVM 调优
java·jvm·后端·面试·架构