为什么HashMap在发生碰撞或最坏情况下会进行调整大小

4
我在Java版本1.7及以下的情况下使用反射来查找HashMap的当前容量。在下面的程序中,将12个唯一的人放入HashMap的一个桶中(使用相同的哈希码)。然后,我将第13个唯一的人放入相同或不同的桶中(使用相同或不同的哈希码)。在添加了第13个元素之后,在这两种情况下,HashMap会调整大小为32个桶。我理解由于负载因子为0.75和初始容量为16,HashMap在第13个元素时会调整大小为其两倍。但是,仍然有空桶可用,只有2个桶用于这13个元素。
我的问题是:
1. 我的理解是否正确?我没有犯任何错误。这是HashMap的预期行为吗? 2. 如果所有这些都正确,即使有12或11个空闲桶,为什么需要在这种情况下通过第13个元素将HashMap扩大一倍呢?调整HashMap大小不是额外的开销或昂贵的吗?在这种情况下,将第13个元素放入任何可用的桶中不是更好吗?
请注意,以上内容保留了HTML标签。
public class HashMapTest {
    public static void main(String[] args)
            throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException {
        HashMap<Person, String> hm = new HashMap<Person, String>();
        for (int i = 1; i <= 12; i++) {
            // 12 Entry in same bucket(linkedlist)
            hm.put(new Person(), "1");
        }
        System.out.println("Number of Buckets in HashMap : " + bucketCount(hm));
        System.out.println("Number of Entry in HashMap :  " + hm.size());
        System.out.println("**********************************");
        // 13th element in different bucket
        hm.put(new Person(2), "2");
        System.out.println("Number of Buckets in HashMap : " + bucketCount(hm));
        System.out.println("Number of Entry in HashMap :  " + hm.size());
    }

    public static int bucketCount(HashMap<Person, String> h)
            throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException {
        Field tableField = HashMap.class.getDeclaredField("table");
        tableField.setAccessible(true);
        Object[] table = (Object[]) tableField.get(h);
        return table == null ? 0 : table.length;
    }
}

class Person {
    int age = 0;

    Person() {
    }

    Person(int a) {
        age = a;
    }

    @Override
    public boolean equals(Object obj) {
        return false;
    }

    @Override
    public int hashCode() {
        if (age != 0) {
            return 1;
        } else {
            return age;
        }
    }
}

输出

Number of Buckets in HashMap : 16
Number of Entry in HashMap :  12
**********************************
Number of Buckets in HashMap : 32
Number of Entry in HashMap :  13

1
@Am_I_Helpful 对于第二个答案,负载因子是基于条目计数工作的。为什么它不看到已经可用的桶呢?为什么它要进行调整大小,从而影响性能呢? - Pradeep Singh
3个回答

5
  1. 是的,这是预期的行为。
  2. HashMap并不关心使用了多少桶。它只知道负载因子已达到,碰撞的概率变得太大,因此应该调整Map的大小。即使已经发生了许多碰撞,调整Map的大小实际上可以修复这个问题。在你的情况下,由于有意选择了相同的hashCode,所以无法修复这个问题。但是在更现实的情况下,hashCode应该具有更好的分布。如果您故意选择糟糕的hashCode,HashMap不能为自己提高效率,在处理HashMap永远无法修复的极端情况时增加复杂性是没有意义的。

1
嗨JB!我注意到一件事情。第13个元素的容量由于负载因子0.75而变为32。现在,如果我从HashMap中逐个删除元素,容量不会恢复到16。即使HashMap现在只包含7个条目,容量仍然是32。如果条目计数下降,HashMap没有必要减少容量吗? - Pradeep Singh
2
不,HashMap会根据需要进行扩展,但永远不会收缩。 - JB Nizet
1
哦,好的,谢谢JB :) - Pradeep Singh
1
@Pradeep 如果你想让哈希表收缩回来,你可以通过实现Map接口来定义自定义的哈希表实现。 - Ansh

3

是的,您观察到的行为是预期的行为。

HashMap 的实现希望您为键使用合理的 hashCode。它假设您的 hashCode 会尽可能均匀地分布在可用的桶中。如果您未能做到这一点(就像您在示例中所做的那样 - 所有键都具有相同的 hashCode),则会导致性能不佳。

在假设均匀分布的情况下,HashMap 在超过负载因子后将其大小加倍是有意义的。它不会检查实际上有多少个桶为空(因为它无法知道新条目是分配给空桶还是占用桶)。它只检查每个桶中的平均条目数。一旦该数字超过负载因子,桶的数量就会加倍。


1
根据您的回答,您说“由于它无法知道新条目是分配给空桶还是已占用的桶”,现在有两个问题。第一个问题是:如何将特定的桶链接到HashCode?我认为首先计算HashCode,然后将其附加/链接到空桶中。如果我错了,请纠正我。第二个问题是:我认为新条目肯定会被分配到空桶或已经占用的桶中。在这两种情况下,都不需要扩大哈希映射的大小。我错了吗?是的,如果所有桶都已满,则将HashMap的大小加倍是有意义的。 - Pradeep Singh
2
@PradeepSingh hashCode是根据hashCode的值(由HashMap实现转换以尝试改善分布)和当前桶的数量映射到一个桶中。无论桶是否已被占用都没有影响。 - Eran
2
@PradeepSingh 至于你的第二个问题,将 HashMap 扩大一倍可以在映射到同一个桶中具有不同哈希码的键时起到帮助作用。当地图扩大时,这些键可能会分别分配到不同的桶中,从而减少在这些桶中搜索的时间,因为新的桶中每个条目都比原始桶中的条目少。 - Eran
1
无法理解当具有不同哈希码的键被映射到同一个桶时的情况。这是怎么发生的?据我所知,不同的哈希码应该映射到同一个桶。 - Pradeep Singh
3
为了简单起见,假设使用 hashCode % numberOfBuckets 来选择桶。现在假设有7个桶和5个hashCode值分别是0、7、14、21和28。所有的对象都会放到同一个桶中,即桶0。现在,如果地图被调整为11个桶大小,那么这些对象将分别放到桶0、0、3、10和6中。 - JB Nizet
显示剩余2条评论

3

这里还有一个小问题,当你调整内部数组的大小(从16变成32)时,你也在“触及”所有条目。让我解释一下:

当有16个桶(内部数组大小为16)时,只有最后4位决定该条目将要到哪里;想一下%,但在内部实际上是(n - 1) & hash,其中n是桶的数量。

当内部数组增长时,会考虑多一个比特位来决定一个条目将要到哪里:以前有4位,现在有5位;这意味着所有条目都会被重新计算哈希值,并有可能移动到不同的桶中;这就是为什么需要调整大小,以分散条目。

如果你真的想填补所有的“差距”,可以指定load_factor1,而不是默认的0.75;但这会对HashMap构造函数中记录的内容产生影响。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接