【问题标题】:Why HashMap initial capacity is not properly handled by the library?为什么 HashMap 初始容量没有被库正确处理?
【发布时间】:2012-10-31 01:21:25
【问题描述】:

要为 N 个元素创建 HashMap/HashSet,我们通常使用new HashMap((int)(N/0.75F)+1),这很烦人。

为什么库一开始就没有处理这个问题,而是允许像new HashMap(N)(不应该重新散列直到N个元素)这样的初始化来处理这个计算(int)(N/0.75F)+1

【问题讨论】:

  • 我不明白问题出在哪里,因为 HashMap 按照你说的做。
  • 只有一个数字的重载指定了包括可用空间在内的大小。 Venkata 希望它是重新散列之前的条目数。库开发人员做出了另一种选择。现在没有必要再讨论这个了。
  • @jackrabit 你说得对。我只是想知道有什么技术原因,为什么会这样设计。

标签: java hashmap capacity


【解决方案1】:

大多数实现会随着您添加更多元素而自动扩展。当容器变满时,大多数实现的性能也会下降。这就是为什么首先有一个负载因素:留下一些可用空间。

【讨论】:

  • 嗯..不确定你是否理解这个问题。如果我创建一个new HashMap(N),我会假设它不会增长/重新散列在我放置 N+1 个元素之前不会发生,但事实是,重新散列会在此之前发生。为了防止重新散列,我们将初始化为new HashMap((int)(N/0.75F)+1)。现在我的问题是图书馆会处理这个问题并允许我们使用new HashMap(N)) 并在内部处理这个计算。
  • 您的问题完全不清楚。请参阅 Tomasz 的回答。他们一定认为你的用例是一个不常见的用例,如果你需要它可以很容易地实现。
  • AFAIK 你甚至不能确定在这样初始化时不会发生重新散列。你真的有一个(可衡量的)问题与重新散列在这里,或者你只是害怕失去性能,如果它发生?否则这似乎是过早优化的情况......
【解决方案2】:

更新

更新以反映已更改的问题。不,没有这样的标准API,但似乎 中有一个方法Maps.newHashMapWithExpectedSize(int)

创建一个HashMap 实例,具有足够高的“初始容量”,它应该容纳expectedSize 元素而不会增长。


我必须将它初始化为 (int)(N/0.75F)+1

不,你没有。如果你从其他Map创建新的HashMapHashMap默认先计算容量:

public HashMap(Map<? extends K, ? extends V> m) {
    this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
                  DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
    putAllForCreate(m);
}

如果你一个一个地添加元素,同样的过程也会发生:

void addEntry(int hash, K key, V value, int bucketIndex) {
    if ((size >= threshold) && (null != table[bucketIndex])) {
        resize(2 * table.length);
        //...
    }

    createEntry(hash, key, value, bucketIndex);
}

使用HashMap(int initialCapacity, float loadFactor) 构造函数的唯一原因是当您从一开始就知道要在HashMap 中存储多少元素,从而避免以后调整大小和重新散列(地图从一开始就具有正确的大小)。

一个有趣的实现细节是初始容量被修剪到最接近的 2 次幂(参见:Why ArrayList grows at a rate of 1.5, but for Hashmap it's 2?):

// Find a power of 2 >= initialCapacity
int capacity = 1;
while (capacity < initialCapacity)
    capacity <<= 1;

因此,如果您希望 HashMap 具有定义的确切容量,只需使用 2 的幂即可。

选择不同的loadFactor 可以让您以空间换取性能 - 较小的值意味着更多的内存,但更少的冲突。

【讨论】:

  • 我只说这个案例new HashMap(N),因为这是我们99%的使用次数。
  • @VenkataRaju:根据您的 cmets,我认为您在将 N 舍入到最接近的 2 次幂 (?) 时遇到问题,请参阅我的答案的更新。
  • @VenkataRaju:看起来Maps.newHashMapWithExpectedSize(int) 是您需要的,请参阅我的更新。
  • 你说得对。我只是想知道是否有任何技术原因,为什么会这样设计。
【解决方案3】:

我已经运行了以下程序

public static void main(String... args) throws IllegalAccessException, NoSuchFieldException {
    for (int i = 12; i < 80; i++) {
        Map<Integer, Integer> map = new HashMap<Integer, Integer>((int) Math.ceil(i / 0.75));
        int beforeAdding = Array.getLength(getField(map, "table"));
        for (int j = 0; j < i; j++) map.put(j, j);
        int afterAdding = Array.getLength(getField(map, "table"));
        map.put(i, i);
        int oneMore = Array.getLength(getField(map, "table"));
        System.out.printf("%,d: initial %,d, after N %,d, after N+1 %,d%n ",
                i, beforeAdding, afterAdding, oneMore);
    }
}

private static <T> T getField(Map<Integer, Integer> map, String fieldName) throws NoSuchFieldException, IllegalAccessException {
    Field table = map.getClass().getDeclaredField(fieldName);
    table.setAccessible(true);
    return (T) table.get(map);
}

打印出来

 12: initial 16, after N 16, after N+1 32
 13: initial 32, after N 32, after N+1 32
 .. deleted ..
 24: initial 32, after N 32, after N+1 64
 25: initial 64, after N 64, after N+1 64
 .. deleted ..
 47: initial 64, after N 64, after N+1 64
 48: initial 64, after N 64, after N+1 128
 49: initial 128, after N 128, after N+1 128
 .. deleted ..
 79: initial 128, after N 128, after N+1 128

这表明默认初始化器的初始容量是四舍五入到二的下一次幂。这个值的问题是,如果你希望这是最终的大小,如果你想避免调整大小,就必须考虑负载因子。理想情况下,您不必这样做,就像 Map 复制构造函数为您所做的那样。

【讨论】:

  • @VenkataRaju 谢谢你的链接。这是多余的。您只需指定 N 作为初始容量。
  • 你只需要指定N作为初始容量 嗯..我不这么认为,那为什么Maps.newHashMapWithExpectedSize(int expectedSize)存在?查看@Tomasz 的更新回复
  • @VenkataRaju 我想我现在明白你的意思了。更新了我的答案。
猜你喜欢
  • 2012-01-11
  • 1970-01-01
  • 2015-01-30
  • 1970-01-01
  • 2014-08-23
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多