一、为什么推荐设置 HashMap 数组大小
(一)性能方面的考虑
-
减少哈希冲突
- HashMap 的核心操作(如
put、get)的性能在很大程度上取决于哈希冲突的程度。当多个键通过哈希函数计算得到相同的数组索引时,就会发生哈希冲突。在这种情况下,这些键值对会以链表(在 Java 8 之前)或者链表 / 红黑树(Java 8 及之后)的形式存储在同一个数组位置上。 - 如果不设置合适的数组大小,随着元素数量的增加,哈希冲突的概率会显著提高。例如,当元素数量远超过数组大小,就像把很多物品塞进一个小盒子,必然会导致物品堆积在某些位置,这会使查找、插入和删除操作的时间复杂度从理想的 O (1) 退化到 O (n)(n 为链表长度或红黑树操作复杂度)。
- 通过预估元素数量并设置合适的数组大小,可以使元素在数组中分布得更均匀,从而减少哈希冲突,保持高效的操作性能。
- HashMap 的核心操作(如
-
避免频繁扩容
- HashMap 在元素数量超过负载因子(默认为 0.75)乘以当前容量时会进行扩容。扩容操作涉及到创建一个新的、更大的数组,并将原数组中的所有元素重新哈希(re - hash)到新数组中。
- 这个过程是比较耗时的,特别是当元素数量较多时。例如,在一个高并发的场景下,如果 HashMap 频繁扩容,会占用大量的 CPU 资源和时间,导致系统性能下降。设置一个合适的初始数组大小可以在一定程度上避免这种频繁的扩容操作,提高系统的稳定性和性能。
(二)内存利用方面的考虑
-
防止内存浪费
- 如果设置的数组大小远远超过实际需要存储的元素数量,会浪费大量的内存空间。例如,在一个小型应用中,只需要存储少量的配置参数(可能不超过 10 个键值对),但如果将 HashMap 的数组大小设置为 1024,那么大部分内存空间都将闲置,这在资源受限的环境(如移动设备或嵌入式系统)中是不可取的。
-
合理利用内存资源
- 根据实际情况设置数组大小可以更合理地利用内存。通过预估元素数量,设置一个稍大于或等于预估数量的 2 的幂次方作为数组大小(后面会讲到为什么是 2 的幂次方),可以在保证性能的同时,有效地利用内存,避免不必要的内存开销。
二、HashMap 默认大小及换算
如果大家看过前一篇文章的话,这里就很容易理解了
(一)默认大小
HashMap 的默认初始容量是 16。这是一个经过综合考虑的默认值,对于许多小规模的应用场景或者对性能要求不是特别高的场景,16 这个容量可以在一定程度上满足需求,并且具有较好的通用性。
(二)换算规则
-
确保为 2 的幂次方
- HashMap 在内部会将用户指定的初始容量转换为大于等于该值的最小的 2 的幂次方。这是因为 HashMap 的内部实现机制依赖于位运算来计算元素在数组中的存储位置,即通过
(n - 1) & hash(n 为数组大小,hash 为键的哈希值)来确定位置。当 n 为 2 的幂次方时,n - 1 的二进制形式所有位都是 1,这样在与 hash 进行位运算时,可以充分利用 hash 值的低位信息,使元素分布更均匀,减少哈希冲突。
- HashMap 在内部会将用户指定的初始容量转换为大于等于该值的最小的 2 的幂次方。这是因为 HashMap 的内部实现机制依赖于位运算来计算元素在数组中的存储位置,即通过
-
具体换算方法(基于源码中的
tableSizeFor方法)- 假设用户指定的初始容量为
cap,在tableSizeFor方法中,首先会执行int n = cap - 1。这一步的目的是处理cap本身已经是 2 的幂次方的情况,例如cap = 8,经过cap - 1后为 7,二进制为111,后续的位运算可以正确地将其转换为 8。 - 然后通过一系列无符号右移(
>>>)和按位或(|)操作,即n |= n >>> 1; n |= n >>> 2; n |= n >>> 4; n |= n >>> 8; n |= n >>> 16;。这些操作的目的是将n的二进制最高位的 1 以及其后面的所有位都变为 1。例如,若cap = 10,cap - 1 = 9(二进制为1001),经过这些位运算后,得到一个二进制数,其最高位的 1 以及后面的位都变为 1,此时n为 15(二进制为1111)。 - 最后通过
return (n < 0)? 1 : (n >= MAXIMUM_CAPACITY)? MAXIMUM_CAPACITY : n + 1;返回实际的初始容量。对于刚才的例子,n + 1后得到 16,这就是最终计算出的大于等于 10 的最小的 2 的幂次方。
- 假设用户指定的初始容量为