Java HashMap 负载因子 Load Factor

HashMap 类属于 Java Collection 框架,提供了Map接口的基本实现。

HashMap 内部结构

HashMap 中需要了解的概念:

  • 哈希 hashing – 一种将对象数据映射到某个具有代表性的整数值的算法。哈希函数应用于键对象以计算桶的索引,以便存储和检索任何键值对。
  • 容量 capacity – 容量是HashMap中桶的数量,默认这个数量为 16。
  • 负载因子 Load Factor – 负载因子是决定何时增加Map容量的度量。默认负载系数为容量的 75%。
  • 临界点 threshold – 临界点是当前容量和负载因子的乘积。
  • 重新哈希 rehashing – 重新哈希是重新计算已存储条目的哈希码的过程。简单地说,当哈希表中的条目数超过阈值时,对Map进行重新哈希处理,使其具有大约两倍于以前的桶数。
  • 碰撞 collision – 当哈希函数为两个不同的键返回相同的桶位置时,就会发生冲突。

我们创建一个如下的 HashMap:

Map<Integer, String> map = new HashMap<>();
map.put(1, "one");
map.put(2, "two");
map.put(3, "three");
map.put(4, "four");

此时,我们可以在 IDE 中看到 Map 的结构:

map {HashMap@488}
loadFactor 0.75
modCount 4
size 4
threshold 12
table HashMap$Node[16]@507
entrySet {HashMap$EntrySet@491} size = 4

自定义参数

上面,我们使用默认构造函数创建了HashMap 。在接下来的部分中,我们将看到如何将初始容量和负载因子传递给构造函数,来创建一个HashMap。

自定义初始容量

创建一个具有 5 个初始容量的 Map :

Map<Integer, String> initMap = new HashMap<>(5);

此时,我们可以在 IDE 中看到 Map 的结构:

map {HashMap@488}
loadFactor 0.75
modCount 4
size 4
threshold 6
table HashMap$Node[8]@490
entrySet {HashMap$EntrySet@491} size = 4

虽然我们指定了初始容量为 5,但是 HashMap 并没有才用我们给的值,table 的大小为 8,临界点threshold 刚好为 8*0.75=6。

创建一个初始容量为 3,负载因子 为 0.5 的 Map:

Map<Integer, String> map = new HashMap<>(3, 0.5f);

此时,我们可以在 IDE 中看到 Map 的结构:

map {HashMap@488}
loadFactor 0.5
modCount 1
size 1
threshold 2
table HashMap$Node[4]@490
entrySet null

虽然我们指定了初始容量为 3,但是 HashMap 并没有才用我们给的值,table 的大小为 4,临界点threshold 刚好为 4*0.5=2。

当我们给初始容量为 3,负载因子 为 0.5 的 Map中添加 4 个元素后结构:

map {HashMap@488}
loadFactor 0.5
modCount 4
size 4
threshold 4
table HashMap$Node[8]@490
entrySet null

可以看到,table 的大小变为 8 了,也就是扩容了,而扩容是一个耗时的行为。

原因分析

虽然随意指定初始容量和负载因子的值,不会产生异常。但是很明显,不合适的取值会导致 HashMap 产生额外的消耗。

HashMap 内部使用哈希码作为存储键值对的基础,如果 hashCode() 方法写得很好,HashMap 将把元素分布在所有的桶中。此时,HashMap 中存储和检索条目将达到固定时间复杂度 O(1)。

当元素数量增加并且桶大小固定时,就会出现问题。这将导致每个桶中装有更多的元素,从而产生较高的时间复杂度。

解决的办法是我们可以在增加元素的时候增加桶 bucket 的个数。然后,我们可以在所有存储桶中重新分配元素。通过这种方式,我们将能够在每个桶中保持恒定数量的项目并保持O(1)的时间复杂度。

合适的负载因子

在这里,负载因子帮助我们决定何时增加桶的数量。负载因子越低,空闲桶就越多,发生碰撞的机会就越少,从而使 Map 实现更好的性能。因此,我们需要保持低负载因子以实现低时间复杂度。

同时也应该注意,HashMap通常具有O(n)的空间复杂度,其中n是条目数。太低的负载因子值会增加空间开销。

当 Map 中的元素数量超过 threshold 限制时, Map的容量会增加一倍。当容量增加时,我们需要将所有元素(包括现有的和新加入的)平均分配到存储桶中。

初始化一个默认的 HashMap,初始容量是 16,负载因子是0.75,临界值为 12,当向其中添加第十三个元素后,Map 的结构:

map {HashMap@488}
loadFactor 0.75
modCount 13
size 13
threshold 24
table HashMap$Node[32]@525
entrySet {HashMap$EntrySet@491} size = 13

哈希冲突优化

当哈希值相同时,就出现冲突了,这会降低 Map 的性能。

最开始,Java 中的 HashMap 通过使用 LinkedList 存储出现冲突的元素。冲突元素会被添加到LinkedList的头部。在最坏的情况下,这会将复杂度增加到O(n)。

为了避免这个问题,Java8 及更高版本使用平衡树(也称为红黑树)而不是LinkedList来存储碰撞元素。这将HashMap的最坏情况性能从 O(n) 提高到 O(logn)。

HashMap最初使用LinkedList,然后当元素的数量超过某个阈值时,它将用平衡二叉树替换 LinkedList。TREEIFY_THRESHOLD 常量决定了这个阈值,目前这个值为 8,这意味着如果同一个桶中有超过 8 个元素,Map将使用红黑树来保存它们。

转载请注明出处:码谱记录 » Java HashMap 负载因子 Load Factor
标签: