为啥 ArrayList 以 1.5 的速度增长,而 Hashmap 却是 2?

Posted

技术标签:

【中文标题】为啥 ArrayList 以 1.5 的速度增长,而 Hashmap 却是 2?【英文标题】:Why ArrayList grows at a rate of 1.5, but for Hashmap it's 2?为什么 ArrayList 以 1.5 的速度增长,而 Hashmap 却是 2? 【发布时间】:2011-06-29 18:30:48 【问题描述】:

根据 Sun Java 实现,在扩展期间,ArrayList 增长到其初始容量的 3/2,而 HashMap 的扩展率是两倍。这背后的原因是什么?

根据实现,对于 HashMap,容量应始终为 2 的幂。这可能是 HashMap 行为的一个原因。但在这种情况下,问题是,对于 HashMap,为什么容量应该始终是 2 的幂?

【问题讨论】:

StringBuffer/StringBuilder 也增长了 2 倍,并且没有要求它的大小必须是 2 的幂。 这可能无非是两个不同的程序员编写了 ArrayList 和 HashMap 的实现,他们都任意决定了不同的增长值。 【参考方案1】:

增加 ArrayList 容量的代价高昂的部分是将支持数组的内容复制一个新的(更大的)内容。

对于 HashMap,它正在创建一个新的后备数组并将所有映射条目放入新数组中。而且,容量越高,碰撞的风险就越低。这更昂贵,并解释了为什么扩展因子更高。 1.5 与 2.0 的原因是什么?我认为这是“最佳实践”或“良好的权衡”。

【讨论】:

即使是ArrayList也可能将容量乘以2,这有什么坏处吗? 危害在于 ArrayList 的大小越大,分配给它的内存就越多(如果不使用空间可能会浪费)。由于增加 ArrayList 的容量比增加 HashMap 的容量要便宜得多,因此在增加 ArrayList 的容量时更加保守是有意义的。本质上,@Andreas_D 解释了为什么 HashMap 的因子应该大于 ArrayList 的因子。为什么是 2.0 和 1.5?这可能是基于使用测试,但我猜你必须问 Java 开发人员自己。 @Arnab Biswas:还有一个原因:ArrayList 中未使用的内存被浪费了,这与HashMap 不同,它降低了冲突率,从而加快了访问速度。 此外,哈希表通常会增加 2 倍(并且支持它的存储桶列表的大小是 2 的幂),因为几乎在每个实现中都使用了一种优化:当哈希计算后,进行模运算 (%) 以找到存储桶以将值放入:bucketIndex = hash % numBuckets。昂贵的x % n 操作可以按位简化为x & (n - 1)但只有当n 是2 的幂时。哈希图/表必须每次增长 2 倍,以保持支持它的桶的二次方大小。 请参阅下面的其他答案。 @Saint 它将产生严格小于n 的索引(只要n 是正数),但它可以跳过存储桶,可能是大多数存储桶(例如,如果n 是一个权力二加 1)【参考方案2】:

对于HashMap,为什么容量总是2的幂?

我能想到两个原因。

    您可以快速确定哈希码进入的存储桶。你只需要一个按位与而不需要昂贵的模数。 int bucket = hashcode & (size-1);

    假设我们的增长因子为 1.7。如果我们从 11 号开始,下一个尺寸将是 18 号,然后是 31 号。没问题。正确的?但是Java中字符串的哈希码是用31的素数计算的。字符串进入的桶,hashcode%31,然后仅由字符串的最后一个字符确定。再见O(1),如果您存储的文件夹都以/ 结尾。例如,如果您使用3^n 的大小,如果您增加n,分布不会变得更糟。从大小39,桶2 中的每个元素现在将转到桶257,具体取决于较高的数字。这就像将每个桶分成三部分。因此,整数增长因子的大小将是首选。 (当然,这一切都取决于您如何计算哈希码,但任意增长因子不会让人感觉“稳定”。)

【讨论】:

关于你的第二个论点: 1. 避免31 很容易。 2.表达式hashcode%31因为负值而不能工作。 3. 像HashMap.hash 这样的一些“哈希强化”可能会有所帮助。 4. 模数可以用(int) ((size * (h & 0xFFFFFFFFL)) >> 32) 之类的东西代替,这在我的电脑上是两倍多。 5. 话虽如此,+1。【参考方案3】:

HashMap 的设计/实现方式,它的底层桶数必须是 2 的幂(即使你给它一个不同的大小,它也会使它成为 2 的幂),因此它每次增长两倍. ArrayList 可以是任意大小,并且在增长方式上可以更加保守。

【讨论】:

【参考方案4】:

接受的答案实际上并没有对问题给出确切的回答,但@user837703 对该答案的评论清楚地解释了为什么 HashMap 以 2 的幂增长。

找到这篇文章,详细解释http://coding-geek.com/how-does-a-hashmap-work-in-java/

让我发布它的片段,它给出了问题的详细答案:

// the function that returns the index of the bucket from the rehashed hash
static int indexFor(int h, int length) 
    return h & (length-1);

为了高效工作,内部数组的大小需要是 2 的幂,让我们看看为什么。

假设数组大小为 17,掩码值为 16(大小 -1)。 16 的二进制表示是 0…010000,因此对于任何哈希值 H,使用按位公式“H AND 16”生成的索引将是 16 或 0。这意味着大小为 17 的数组将仅用于2 个桶:一个在索引 0 和一个在索引 16,效率不是很高……

但是,如果您现在采用 2 的幂(例如 16)的大小,则按位索引公式为“H AND 15”。 15 的二进制表示为 0…001111,因此索引公式可以输出 0 到 15 的值,并且完全使用大小为 16 的数组。例如:

如果 H = 952 ,其二进制表示为 0..01110111000,相关索引为 0…01000 = 8 如果 H = 1576,其二进制表示为 0..011000101000,则关联索引为 0…01000 = 8 如果H = 12356146,则其二进制表示为0..0101111001000101000110010,关联索引为0…00010 = 2 如果H = 59843,其二进制表示为0..01110100111000011,相关索引为0…00011 = 3

这就是为什么数组大小是 2 的幂。这种机制对开发者来说是透明的:如果他选择一个大小为 37 的 HashMap,Map 会自动选择 37 之后的 2 的下一个幂(64)作为其内部数组的大小。

【讨论】:

而arraylist的增长速度可能是,如果我们将arraylist的容量翻倍,我们将无法使用之前使用的内存。示例:arraylist 的初始容量 = 10(内存位置 = 1 to 10)。加倍后,arraylist 的容量 = 20(内存位置 = 11 to 30)。加倍后,arraylist 的容量 = 40(内存位置 = 31 to 70)。加倍后,arraylist的容量=80(内存位置=71 to 150 但是如果我们使用 1.5 作为增长率。示例:初始容量 = 10(内存位置 = 1 to 10)。将容量增加 1.5 后,arraylist 的容量 = 15**(内存位置 = 11 to 25)。在将容量进一步增加 1.5 时,**arraylist 的容量 = 22(内存位置 = 26 to 47)。在将容量进一步增加 1.5 时,arraylist 的容量 = 33(内存位置 = 48 to 80)。在将容量进一步增加 1.5 时,arraylist 的容量 = 49(内存位置 = 81 to 129)。在下一条评论中继续 在将容量进一步增加 1.5 时,arraylist 的容量 = 73(内存位置 = 1 to 73)。在这里,我们可以利用以前使用的内存位置从 1 到 73【参考方案5】:

散列利用将数据均匀分布到存储桶中。该算法试图防止桶中的多个条目(“哈希冲突”),因为它们会降低性能。

现在,当达到 HashMap 的容量时,会扩展大小并使用新的存储桶重新分配现有数据。如果 size-increas 太小,这种空间的重新分配和重新分配就会过于频繁地发生。

【讨论】:

虽然这解释了基本原理,但并没有真正解释为什么 HashMap 将大小乘以 2 而不是像 ArrayList 那样乘以 1.5(例如)。【参考方案6】:

避免在地图上发生冲突的一般规则是将最大负载因子保持在 0.75 左右 为了减少冲突的可能性并避免昂贵的复制过程,HashMap 以更大的速度增长。

正如@Peter 所说,它必须是 2 的幂。

【讨论】:

【参考方案7】:

我无法告诉你为什么会这样(你必须询问 Sun 开发人员),但要了解这是如何发生的,请查看源代码:

    HashMap:看看 HashMap 如何调整到新的大小(source line 799)

         resize(2 * table.length);
    

    ArrayList:source,第 183 行:

    int newCapacity = (oldCapacity * 3)/2 + 1;
    

更新:我错误地链接到 Apache Harmony JDK 的来源 - 将其更改为 Sun 的 JDK。

【讨论】:

谢谢彼得,我之前已经检查过源代码。但这并没有帮助我理解 API 开发者的意图。 顺便说一句:OpenJDK(以及 Oracle JDK)使用一些完全不同的代码,但实际上也增加了一半的大小。 对于ArrayList,现在使用效率稍高的int newCapacity = oldCapacity + (oldCapacity >> 1);计算新容量

以上是关于为啥 ArrayList 以 1.5 的速度增长,而 Hashmap 却是 2?的主要内容,如果未能解决你的问题,请参考以下文章

为啥使用不同的 ArrayList 构造函数会导致内部数组的增长率不同?

OpenCV这么简单为啥不学——1.5解决putText中文乱码问题

为啥要以初始容量启动 ArrayList?

以 1.5x、2x 速度播放视频文件

为啥我的程序在字节数组中保存 1.5Gb 的内存?

HashMap为啥比数组查询速度快?