在HashMap中使用字符串键的坏主意?

2022-08-31 14:00:14

我知道String类的hashCode()方法不能保证为不同的String-s生成唯一的哈希代码。我看到很多将字符串键放入HashMap-s(使用默认的String hashCode()方法)的用法。如果映射替换了以前使用真正不同的字符串键放入映射的 HashMap 条目,则这种用法中的许多用法可能会导致严重的应用程序问题。put

遇到 String.hashCode() 为不同的 String-s 返回相同值的几率有多大?当密钥是字符串时,开发人员如何变通解决此问题?


答案 1

开发人员不必为了实现程序的正确性而解决HashMap中的哈希冲突问题。

这里有几个关键的事情需要了解:

  1. 冲突是哈希的固有特征,而且必须如此。可能值的数量(在本例中为字符串,但它也适用于其他类型)远远大于整数范围。

  2. 哈希的每次使用都有一种处理冲突的方法,Java集合(包括HashMap)也不例外。

  3. 哈希不涉及相等性测试。确实,相等的对象必须具有相等的哈希码,但事实并非如此:许多值将具有相同的哈希码。因此,不要尝试使用哈希码比较来代替相等。收藏集没有。他们使用哈希来选择子集合(在 Java 集合世界中称为存储桶),但他们使用 .equals() 来实际检查是否相等。

  4. 您不仅不必担心冲突导致集合中不正确的结果,而且对于大多数应用程序,您通常也不必担心性能 - Java哈希集合在管理哈希码方面做得很好。

  5. 更好的是,对于您询问的情况(字符串作为键),您甚至不必担心哈希码本身,因为Java的String类会生成一个非常好的哈希码。大多数提供的 Java 类也是如此。

如果需要,可以更详细地了解一些详细信息:

散列的工作方式(特别是在像Java的HashMap这样的散列集合的情况下,这是你问的)是这样的:

  • HashMap 将您为其提供的值存储在子集合(称为存储桶)中。这些实际上是作为链表实现的。这些项目的数量有限:iirc,默认为 16 个,并且随着您将更多项目放入地图中,该数量会增加。存储桶应始终多于值。举个例子,使用默认值,如果向 HashMap 添加 100 个条目,则将有 256 个存储桶。

  • 每个可以用作映射中键的值都必须能够生成一个整数值,称为哈希码。

  • 哈希映射使用此哈希代码来选择存储桶。最终,这意味着将整数值取为桶的数量,但在此之前,Java的HashMap有一个内部方法(称为),它可以调整哈希码以减少一些已知的聚集源。modulohash()

  • 查找值时,HashMap 选择存储桶,然后使用 对链接列表进行线性搜索来搜索单个元素。.equals()

所以:你不必为了正确性而解决冲突,你通常也不必担心它们的性能,如果你使用的是原生Java类(如String),你也不必担心生成哈希码值。

在你必须编写自己的哈希码方法的情况下(这意味着你已经编写了一个具有复合值的类,如名字/姓氏对),事情会变得稍微复杂一些。这里很可能弄错了,但这不是火箭科学。首先,知道这一点:为了确保正确性,您唯一必须做的就是确保相等的对象产生相等的哈希码。因此,如果为类编写哈希码()方法,则还必须编写 equals() 方法,并且必须检查每个方法中的相同值。

可以编写一个糟糕但正确的hashcode()方法,我的意思是它将满足“相等的对象必须产生相等的哈希码”的约束,但由于发生大量冲突,仍然表现非常差。

规范退化的最坏情况是编写一个方法,该方法对所有情况仅返回一个常量值(例如,3)。这意味着每个值都将被散列到同一个存储桶中。

它仍然有效,但性能会降低到链表的性能。

显然,你不会写出这样一个可怕的哈希码()方法。如果您使用的是一个像样的IDE,它能够为您生成一个IDE。由于StackOverflow喜欢代码,因此下面是上面名字/姓氏类的代码。


public class SimpleName {
    private String firstName;
    private String lastName;
    public SimpleName(String firstName, String lastName) {
        super();
        this.firstName = firstName;
        this.lastName = lastName;
    }
    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result
                + ((firstName == null) ? 0 : firstName.hashCode());
        result = prime * result
                + ((lastName == null) ? 0 : lastName.hashCode());
        return result;
    }
    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        SimpleName other = (SimpleName) obj;
        if (firstName == null) {
            if (other.firstName != null)
                return false;
        } else if (!firstName.equals(other.firstName))
            return false;
        if (lastName == null) {
            if (other.lastName != null)
                return false;
        } else if (!lastName.equals(other.lastName))
            return false;
        return true;
    }
}


答案 2

我引导你到这里的答案。虽然使用字符串不是一个主意(@CPerkins完美地解释了原因),但将值存储在具有整数键的哈希图中更好,因为它通常更快(尽管不明显)并且具有较低的碰撞机会(实际上没有机会)。

在每种情况下,使用216553键查看此冲突图表(从本文中窃取,重新格式化以进行讨论)

Hash           Lowercase      Random UUID  Numbers 
=============  =============  ===========  ==============
Murmur            145 ns      259 ns          92 ns
                    6 collis    5 collis       0 collis
FNV-1a            152 ns      504 ns          86 ns
                    4 collis    4 collis       0 collis
FNV-1             184 ns      730 ns          92 ns
                    1 collis    5 collis       0 collis*
DBJ2a             158 ns      443 ns          91 ns
                    5 collis    6 collis       0 collis***
DJB2              156 ns      437 ns          93 ns
                    7 collis    6 collis       0 collis***
SDBM              148 ns      484 ns          90 ns
                    4 collis    6 collis       0 collis**
CRC32             250 ns      946 ns         130 ns
                    2 collis    0 collis       0 collis

Avg Time per key    0.8ps       2.5ps         0.44ps
Collisions (%)      0.002%      0.002%         0%

当然,整数的数量限制为2 ^ 32,其中字符串的数量没有限制(并且可以存储在a中的键的数量没有理论限制)。如果你使用一个(甚至是一个),碰撞将是不可避免的,因此没有比字符串“更好”的了。但是,即使存在哈希冲突,也会始终放置/获取正确的键值对(请参阅下面的编辑)。HashMaplongfloatput()get()

最后,这真的无关紧要,所以使用任何更方便的东西。但是,如果便利性没有区别,并且您不打算拥有超过2 ^ 32个条目,我建议您用作键。ints


编辑

虽然上述情况绝对正确,但出于性能原因,切勿使用“StringKey”.hashCode()生成一个密钥来代替原始密钥 - 2个不同的字符串可以具有相同的哈希代码,从而导致覆盖您的方法。Java的实现足够智能,可以自动处理具有相同哈希码的字符串(实际上任何类型的键),因此让Java为您处理这些事情是明智的。Stringput()HashMap