面试题答案
一键面试1. 触发扩容的条件
- 容量达到阈值:HashMap有两个重要参数,容量(capacity)和负载因子(load factor)。当HashMap中存储的键值对数量(size)达到容量(capacity)与负载因子(load factor)的乘积时,即
size >= threshold = capacity * loadFactor
,就会触发扩容。默认情况下,HashMap的初始容量为16,负载因子为0.75。
2. 扩容的具体过程
- 创建新数组:扩容时,会创建一个新的Entry数组,其大小是原来数组大小的两倍。例如,初始容量为16,扩容后变为32。
- 重新计算哈希值和索引位置:遍历旧数组中的所有元素,重新计算每个元素在新数组中的哈希值和索引位置。这是因为数组大小发生了变化,哈希值对应的索引也会改变。在Java 8之前,重新计算索引是通过
e.hash & (newCapacity - 1)
来确定新位置;Java 8及之后,为了减少重新计算哈希值的开销,对于某些情况可以直接使用原来的索引或原来索引加上旧容量的位置。例如,如果旧容量为16(二进制10000),新容量为32(二进制100000),那么原来索引为5(二进制00101)的元素,在新数组中的索引可能还是5,也可能是5 + 16 = 21,具体取决于哈希值的高位。 - 迁移元素:将旧数组中的元素逐个迁移到新数组对应的位置上。在Java 8之前,迁移过程采用头插法,在多线程环境下可能会形成环形链表导致死循环;Java 8开始采用尾插法,避免了这个问题。
3. 扩容对HashMap性能产生的影响
- 时间开销:扩容过程涉及创建新数组、重新计算哈希值和迁移元素,这些操作都需要消耗时间,尤其是当HashMap中元素较多时,扩容会带来明显的性能开销。
- 空间开销:在扩容前,如果HashMap已经接近满载,那么扩容时会额外占用大约一倍的空间(新数组创建后,旧数组还未释放),直到迁移完成旧数组才会被垃圾回收。
4. 实际应用中避免频繁扩容带来的性能问题的方法
- 预估容量:在创建HashMap时,如果能够预估存储元素的数量,可以通过构造函数指定初始容量。例如,预计存储100个元素,负载因子为0.75,那么初始容量至少应为
(int)(100 / 0.75) + 1 = 134
,这样可以减少扩容的次数。 - 调整负载因子:如果对空间比较敏感,可以适当提高负载因子,使得HashMap在达到更高的满载程度时才进行扩容,但这可能会增加哈希冲突的概率,影响查找性能;如果对查找性能要求极高,可以适当降低负载因子,让HashMap在较低的满载程度时就扩容,以减少哈希冲突。
- 使用其他数据结构:在某些场景下,如果对插入和删除操作频繁,并且对空间利用率和查找性能有特定要求,可以考虑使用
LinkedHashMap
或TreeMap
等其他数据结构。LinkedHashMap
可以保持插入顺序或访问顺序,TreeMap
可以对键进行排序,它们的实现机制与HashMap
不同,性能特点也有所差异。