索引列表时的最佳 HashMap 初始容量

Posted

技术标签:

【中文标题】索引列表时的最佳 HashMap 初始容量【英文标题】:Best HashMap initial capacity while indexing a List 【发布时间】:2013-03-28 11:09:46 【问题描述】:

我有一个列表 (List<T> list),我想使用映射 (HashMap<Integer, T> map) 通过对象的 ID 对其对象进行索引。我总是在HashMap 构造函数中使用list.size() 作为初始容量,如下面的代码所示。这是在这种情况下使用的最佳初始容量吗?

注意:我永远不会向地图添加更多项目。

List<T> list = myList;
Map<Integer, T> map = new HashMap<Integer, T>(list.size());
for(T item : list) 
    map.put(item.getId(), item);

【问题讨论】:

我建议:1) 将您的变量声明为 Map 而不是 HashMap,2) 如果您注意到 使用分析器,请让 JVM 出现此类问题> 这会给你的表现带来冲击,然后开始评估它。 @LuiggiMendoza 通常是的,同意,但这是一个常见的用例,我们也可以摆脱重新调整大小 【参考方案1】:

如果您希望避免重新散列HashMap,并且您知道不会将其他元素放入HashMap,那么您必须考虑负载因子以及初始容量。负载系数for a HashMap defaults to 0.75。

每当添加新条目时,都会进行计算以确定是否需要重新散列,例如put 放置一个新的键/值。因此,如果您指定初始容量为list.size(),负载因子为1,那么它将在最后一个put 之后重新散列。因此,为了防止重新散列,请使用 1 的负载因子和list.size() + 1 的容量。

编辑

查看HashMap源代码,如果old大小达到或超过阈值,它将重新hash,因此它不会在最后一个put上重新hash。所以看起来list.size() 的容量应该没问题。

HashMap<Integer, T> map = new HashMap<Integer, T>(list.size(), 1.0);

这是HashMap源代码的相关部分:

void addEntry(int hash, K key, V value, int bucketIndex) 
    Entry<K,V> e = table[bucketIndex];
    table[bucketIndex] = new Entry<>(hash, key, value, e);
    if (size++ >= threshold)
        resize(2 * table.length);

【讨论】:

只有我还是没有人知道加载 1.0 是一个非常糟糕的主意?! @rgettman:如果你知道哈希映射在内部是如何工作的,你会注意到你不仅搞砸了插入,而且还搞砸了读取。所有操作都将变为 O(N) 而不是 O(1),因为到处都有碰撞,你必须从一个桶跳到另一个桶 你知道你设置的正确答案是错误答案吗? 请注意,初始容量将在内部四舍五入为二的下一个幂。因此,200 的容量将四舍五入为 256。如果 HashMap 不四舍五入为 capacity 的二次幂值,则将永远不会使用某些存储桶。放置地图数据的桶索引由bucketIndex = hashCode(key) &amp; (capacity-1) 确定。这对于 Java 7 和 8 都是如此,afaik。 使用负载因子 1 是一个坏主意,考虑到如果空闲桶越少,哈希冲突就会增加。 Java 设计人员默认使用 0.75 的负载因子是空间和时间之间的权衡。如果您不确定负载因子内部,请不要触摸此默认值。现在,如果您使用 0.75 而不是 1 的负载因子,则可以使用 initialCapacity = (预期的元素数/0.75)+1 计算不应该导致它重新散列的地图容量。期间。【参考方案2】:

“容量”关键字的定义不正确,并且未按通常预期的方式使用。

默认情况下,HashMap 的“负载因子”为 0.75,这意味着当 HashMap 中的条目数达到所提供容量的 75% 时,它将调整数组大小并重新散列。

例如,如果我这样做:

Map<Integer, Integer> map = new HashMap<>(100);

当我添加第 75 个条目时,映射会将条目表的大小调整为 2 * map.size()(或 2 * table.length)。所以我们可以做一些事情:

    更改负载系数 - 这可能会影响地图的性能 将初始容量设置为 list.size() / 0.75 + 1

最好的选择是两者中的后者,让我解释一下这里发生了什么:

list.size() / 0.75

这将返回 list.size() + list.size() 的 25%,例如,如果我的列表的大小为 100,它将返回 133。然后我们将其添加 1,因为如果大小调整了地图的大小其中等于初始容量的 75%,所以如果我们有一个大小为 100 的列表,我们会将初始容量设置为 134,这意味着从列表中添加所有 100 个条目不会导致任何大小调整地图。

最终结果:

Map<Integer, Integer> map = new HashMap<>(list.size() / 0.75 + 1);

【讨论】:

查看JDK源代码,实际表大小四舍五入到最接近的2的幂。另外,re。您的声明“默认情况下,HashMap 的‘负载因子’为 0.75,这意味着当 HashMap 中的条目数达到所提供容量的 75% 时,它将调整数组大小并重新散列。” - 有点迂腐,仅当条目超过(未达到)容量的 75% 时才会发生调整大小。因此,例如,指定初始容量为 64,负载因子为 0.5,您可以在不调整大小的情况下放入 32 个条目。 也是 100 / 0.75 = 133,并不是说它改变了什么【参考方案3】:

Guava 的 Maps.newHashMapWithExpectedSize 使用此辅助方法来计算默认负载因子 0.75 的初始容量,基于一些预期的值数量:

/**
 * Returns a capacity that is sufficient to keep the map from being resized as
 * long as it grows no larger than expectedSize and the load factor is >= its
 * default (0.75).
 */
static int capacity(int expectedSize) 
    if (expectedSize < 3) 
        checkArgument(expectedSize >= 0);
        return expectedSize + 1;
    
    if (expectedSize < Ints.MAX_POWER_OF_TWO) 
        return expectedSize + expectedSize / 3;
    
    return Integer.MAX_VALUE; // any large value

参考:source

来自newHashMapWithExpectedSize 文档:

创建一个HashMap 实例,具有足够高的“初始容量” 它应该保持expectedSize元素没有增长。这种行为 不能广泛保证,但观察到对于 开放JDK 1.6。也不能保证该方法不是 无意中过大返回的地图。

【讨论】:

+1。对于那些不想了解地图内部结构而只想按预期工作的人来说,这是最简单、最简单的解决方案。【参考方案4】:

你正在做的很好。通过这种方式,您可以确定哈希映射 至少 有足够的容量用于初始值。如果您有更多关于哈希映射使用模式的信息(例如:它是否经常更新?是否经常添加许多新元素?),您可能希望设置更大的初始容量(例如,list.size() * 2),但永远不要降低。使用分析器确定初始容量是否过快不足。

更新

感谢@PaulBellora 建议将初始容量设置为(int)Math.ceil(list.size() / loadFactor)(通常,默认加载因子为 0.75)以避免初始调整大小。

【讨论】:

“哈希映射至少有足够的容量容纳初始值”——我认为默认加载因子为 0.75 时这是不正确的。 @PaulBellora 初始容量与initialCapacity 参数中指定的容量相同。负载因子是衡量哈希表在其容量(初始或非初始)自动增加之前允许达到的程度 对,所以在负载因子为0.75 和初始容量为n 的情况下,输入n 值会导致它调整大小。 @PaulBellora 所以你建议初始容量应该是 size()/.75 以避免初始调整大小?有道理,我会更新我的答案 @italo 在这种情况下,rgettman 的答案和我自己的答案都是等价的。另外,如果你想强制 hashmap 永远不会改变的不变量,也许你应该使用 Collections.unmodifiableMap() 使它不可变【参考方案5】:

根据reference documentation of java.util.HashMap:

在设置其初始容量时应考虑map中预期的条目数及其负载因子,以尽量减少rehash操作的次数。如果初始容量大于最大条目数除以负载因子,则不会发生重新哈希操作。

这意味着,如果您提前知道 HashMap 应该存储多少条目,则可以通过选择适当的初始容量和加载因子来防止重新散列。然而:

作为一般规则,默认负载系数 (.75) 在时间和空间成本之间提供了良好的折衷。较高的值会减少空间开销,但会增加查找成本(反映在 HashMap 类的大部分操作中,包括 get 和 put)。

【讨论】:

【参考方案6】:

如果您不了解负载系数/容量内部结构,则经验法则:

initialCapacityToUse = (Expected No. of elements in map / 0.75) + 1

使用此初始容量值,将不会发生重新哈希以存储给定的预期数量。地图中的元素。

【讨论】:

您能解释一下为什么我们需要 +1 吗?

以上是关于索引列表时的最佳 HashMap 初始容量的主要内容,如果未能解决你的问题,请参考以下文章

HashMap和TreeMap的区别

HashMap和TreeMap的区别

在元素的装载数量明确的时候HashMap的大小应该如何选择。

读源码 HashMap Java8

为什么HashMap建议初始化容量,且容量为2的次幂?

关于HashMap容量的初始化,还有这么多学问。