【HashMap全面知识点】— 快速理解HashMap
目录
一、核心概念
1.定义
2.核心参数
二、底层数据结构与演进
结构细节:
三、核心算法与实现
1.哈希值计算
2.索引计算
3.put方法流程(JDK 1.8)
4.扩容(resize)机制
四、JDK 1.7 与 JDK 1.8 核心差异
五、并发隐患与解决方案
1.并发问题
2.解决方案
六、高频面试题
1.HashMap与Hashtable的区别?
2.为什么容量必须是2的幂?
3.HashMap中Key可以是任何对象吗?
4.HashMap迭代器的fail-fast机制?
5.红黑树转换条件?
七、实践建议
一、核心概念
1.定义
实现Map接口,用于存储键值对(key-value),允许null键(仅一个)和null值,非线程安全,元素无序(插入顺序与遍历顺序不一致)
2.核心参数
- capacity:哈希桶数组容量(默认16,必须为2的幂,最大2³⁰)
- LoadFactor:负载因子(默认0.75),衡量数组填充程度(填充程度=实际元素数量(size)/数组容量(capacity))
- threshold:扩容阈值(capacity*LoadFactor),元素数量超过此值时触发扩容
- size:实际存储的键值对数量
举例:
当数组容量为 16,负载因子 0.75 时,扩容阈值 = 16 × 0.75 = 12
当元素数量达到 12 时,填充程度 = 12/16 = 0.75,此时触发扩容(容量变为 32),避免填充程度过高导致性能下降
二、底层数据结构与演进
版本 | 数据结构 | 红黑树支持 | 链表插入方式 |
---|---|---|---|
JDK 1.7 | 数组+单向链表 | 不支持 | 头插法 |
JDK 1.8 | 数组+单向链表+红黑树 | 支持 | 尾插法 |
结构细节:
- 哈希桶数组(table):存储节点的数组,每个元素是链表或红黑树的头结点
- 链表:解决哈希冲突,相同索引的元素以链表形式存储
- 红黑树:JDK 1.8 新增,当链表长度>=8且数组容量>=64时,链表转为红黑树查询(时间复杂度从O(n)优化为O(logn));当节点数<=6时,红黑树转为链表
三、核心算法与实现
1.哈希值计算
JDK 1.8 对hashCode()进行二次处理,增强随机性
static final int hash(Object key) {int h;return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); // 高16位与低16位异或
}
2.索引计算
通过哈希值与数组长度减一的与运算确定索引
int index = (table.length - 1) & hash; // 等价于 hash % table.length(仅当长度为2的幂时)
数组长度为2的幂保证table.length - 1二进制全为1,使索引分布更均匀
3.put方法流程(JDK 1.8)
- 计算 key 的哈希值。
- 若数组为空,初始化数组(触发 resize())。
- 根据索引定位位置:
- 位置为空:直接插入新节点。
- 位置非空:
- 若 key 已存在(哈希值和
equals
均匹配),覆盖旧值。 - 若为红黑树节点,调用树插入方法。
- 若为链表节点,遍历链表插入,长度达标则转红黑树。
- 若 key 已存在(哈希值和
- 元素数量超过阈值时触发扩容
4.扩容(resize)机制
- 触发条件: size > threshold
- JDK 1.7:新容量 = 原容量 * 2,重新计算所有元素的哈希值和索引,头插法迁移节点
- JDK 1.8:新容量 = 原容量 * 2,通过哈希值高位判断索引(无需重算哈希),尾插法迁移节点,红黑树可能拆分
四、JDK 1.7 与 JDK 1.8 核心差异
特性 | JDK 1.7 | JDK 1.8 |
---|---|---|
数据结构 | 数组 + 链表 | 数组 + 链表 + 红黑树 |
链表插入方式 | 头插法(容易成环) | 尾插法(避免环) |
哈希计算 | 直接使用hashCode() | 二次哈希(高16位异或低16位) |
扩容索引计算 | 重新计算哈希值 | 利用高位判断(优化性能) |
冲突查询效率 | O(n)(链表) | O(logn)(红黑树) |
JDK 1.7 中,HashMap 扩容时会将原数组中的链表节点迁移到新数组,采用头插法(新节点插入到链表头部)。在多线程并发扩容时,可能出现链表成环,进而导致死循环。
JDK 1.8 改用尾插法(新节点插入到链表尾部),迁移节点时保持原链表的顺序,从根本上避免了环的产生。
五、并发隐患与解决方案
1.并发问题
- JDK 1.7:扩容时头插法可能导致链表成环,引发死循环;多线程put可能导致数据丢失
- JDK 1.8:尾插法避免了环问题,但仍存在数据覆盖(如果同时插入相同的key)和size计数不准的问题
2.解决方案
- 使用Collections.synchronizedMap(new HashMap<>())(全局加锁,效率低)
- 使用ConcurrentHashMap(JDK 1.8 采用CAS + 局部锁,效率高)
六、高频面试题
1.HashMap与Hashtable的区别?
区别 | HashMap | Hashtable |
---|---|---|
线程安全 | 非线程安全 | 安全(方法加synchronized) |
null值 | 允许null键值 | 不允许 |
容量 | 初始容量16(扩容*2) | 初始容量11(扩容*2+1) |
2.为什么容量必须是2的幂?
保证(n-1) & hash 能充分利用哈希值的低位,减少冲突;扩容时可通过高位快速计算新索引
3.HashMap中Key可以是任何对象吗?
可以,但是需要重写hashCode()和equals()方法
(key为什么要重写hashCode()和equals()?)
- hashCode() 保证相同对象返回相同哈希值,equals()保证逻辑相同的对象被视为同一key
- 若不重写,可能导致相同逻辑的key被视为不同的键,或不同的key被误判为相同的键
4.HashMap迭代器的fail-fast机制?
迭代过程中若结构被修改(如put/remove),会抛出ConcurrentModificationException,通过modCount变量实现(每次修改递增,迭代时校验)
5.红黑树转换条件?
- 链表转红黑树:链表长度 >= 8 且数组长度 >= 64
- 红黑树转链表:节点数 <= 6
6.HashMap和TreeMap的使用场景?
- HashMap:追求查询/插入效率,无需排序
- TreeMap:需要键有序(自然排序或自定义排序),查询效率略低
7.JDK 1.8对HashMap的优化?
- 引入红黑树,优化链表长查询性能
- 扩容时使用尾插法避免链表成环
- 计算新索引是通过高位判断,无需重新计算哈希值
七、实践建议
- 初始容量设置:已知元素数量时,设为(int)(expectedSize / 0.75 + 1),避免频繁扩容
- key选择:优先使用不可变对象(如String、Integer),避免哈希值变化导致查找失败
- 遍历方式:推荐entrySet()(同时获取键值对,效率高于keySet())
如果您觉得这篇文章对您有帮助,请点赞关注,我会持续分享更多实用的技术文章。如有任何问题,欢迎在评论区留言讨论。