往期回顾
[Java基础] 基本数据类型
[Java基础] 运算符
[Java基础] 流程控制
[Java基础] 面向对象编程
[Java基础] 集合框架
[Java基础] 输入输出流
[Java基础] 异常处理机制
[Java基础] Lambda 表达式
目录
Java HashMap 的数据结构和底层原理
JDK 8, JDK 11, JDK 17 中的变化
常见面试题及答案
Java HashMap 的数据结构和底层原理
数据结构:
HashMap
内部维护了一个数组,称为桶(bucket)或槽(slot),每个桶可以存储一个或多个键值对。- 每个桶默认是链表形式,当桶内的元素数量超过一定阈值时(通常是8),链表会被转换为红黑树以提高查找效率。
哈希函数:
- 当向
HashMap
中插入键值对时,首先会调用键的hashCode()
方法来获取哈希码。 - 然后通过哈希码与当前容量减一进行按位与操作来确定键值对应该存放在哪个桶中。
解决哈希冲突:
- 如果两个不同的键具有相同的哈希码,那么它们就会被放置在同一个桶内。
- 在 JDK 8 之前版本中,使用单向链表处理哈希冲突。
- 在 JDK 8 及之后版本中,如果桶中的节点数超过 TREEIFY_THRESHOLD (默认为8),则链表会被转换成红黑树。
扩容机制:
- 当
HashMap
中的元素数量超过当前容量乘以负载因子(默认为0.75)时,就会触发扩容。 - 扩容通常是将现有容量翻倍,并重新分配所有的元素到新的桶数组中。
JDK 8, JDK 11, JDK 17 中的变化
- JDK 8:
- 引入了红黑树优化。当某个桶中的节点数达到TREEIFY_THRESHOLD时,该桶从链表转换为红黑树。
- 增加了懒加载特性,即初始化时不创建桶数组,而是在第一次插入元素时才创建。 - JDK 11:
- 主要是性能上的微调和一些内部实现细节的优化。
- 对于安全性和稳定性的改进也间接影响了HashMap
的表现。 - JDK 17:
- 没有对HashMap
进行重大功能更新,但持续进行了性能和内存管理方面的优化。
- 包括垃圾收集器(如ZGC)的改进,这可能间接提高了HashMap
的性能。
常见面试题及答案
- 什么是哈希碰撞?如何解决?
- 答:哈希碰撞是指不同的键产生了相同的哈希码,导致它们映射到了HashMap
的同一个桶上。JavaHashMap
通过链表(或红黑树)来解决这个问题,将发生碰撞的键值对链接起来。 - 为什么
HashMap
不保证顺序?
- 答:因为HashMap
是基于哈希表实现的,其内部的数据存储位置依赖于键的哈希码。随着添加、删除等操作,可能会导致哈希码重新计算,从而改变元素的位置。因此,HashMap
不保证任何特定的顺序。 HashMap
** 和Hashtable
有什么区别?**
- 答:HashMap
是非线程安全的,允许 null 键和 null 值;而Hashtable
是线程安全的,不允许 null 键和 null 值。此外,HashMap
提供了更多的方法来控制容量增长策略。HashMap
** 的初始容量是多少?负载因子的作用是什么?**
- 答:HashMap
的默认初始容量是 16,默认负载因子是 0.75。负载因子用于决定何时需要扩容。当元素数量达到容量乘以负载因子时,HashMap
会自动扩容。- 为什么
HashMap
使用红黑树而不是 AVL 树?
- 答:红黑树提供了更好的最坏情况下的性能保障,而且对于插入和删除操作来说,红黑树的旋转次数更少,因此在频繁变更的情况下更为合适。此外,红黑树在实际应用中通常比 AVL 树有更好的平衡性,更适合于动态数据集。 HashMap
中put是如何实现的?
- 答:首先计算key的hashCode值,其次根据hashCode值确定元素在数组中的位置,接着如果位置为空,则直接添加元素,最后如果位置已有元素,则判断key是否相同(地址相同或equals方法返回true),若相同则替换旧值;若不同,则采用链表或红黑树处理碰撞。- 为什么
HashMap
的默认初始化长度为16,并且每次扩容都是2倍?
- 答:第一,为了数据的均匀分布和减少哈希碰撞,HashMap
的默认初始化长度选择为2的幂(如16)。第二,每次扩容为2倍可以确保新数组的长度仍然是2的幂,从而方便使用位运算来确定元素在数组中的位置。