【问题标题】:Overridden equals not mandatory for efficient hashing keys对于有效的散列键,覆盖等于不是强制性的
【发布时间】:2014-01-13 16:41:35
【问题描述】:

在我看到的所有关于重写 equals 和 hashcode 方法的问题中,人们总是说如果你重写 equals 方法,你应该重写 hashcode 方法,反之亦然,以避免在从 Hash 集合中获取对象时出现问题。

我个人认为反之亦然不适合这个目的,让我们考虑我们使用一个对象(带有属性)作为 HashMap 中的键,我们不需要测试它的两个实例之间的相等性对象。

如果我们以有效的方式(基于属性和其他规则)覆盖 hashcode 方法,那么我们可以拥有唯一的键,在这种情况下,对于 HashMap,我们将在每个桶中拥有唯一的值,而 HashMap 不会使用 equals 方法比较桶中的值,因为我们在每个桶中都有一个值。

合约方:

  • 我们的对象的两个实例是相等的(Object.equals 之一)意味着它们具有相同的引用,并且根据我们的 hashcode 方法,hashcode 将是相同的 ==> OK
  • 不同的哈希码会导致对象不相等 ==> NOK(在我们的例子中,可能会违反此规则),但由于我们有良好的哈希键(我们的场景目的),因此合约并不重要

我错了吗?

编辑:(经过搜索,下面的答案,并查看HashMap的源代码)

为什么我们还要重写 equals 方法?

初始条件、唯一的哈希码和没有覆盖 equals()

  • 使用 get() 方法检索值的问题:

Object.equals() 进行引用比较,该方法用于put和get方法中

public V get(Object key) {
    if (key == null)
        return getForNullKey();
    int hash = hash(key.hashCode());
    for (Entry<K,V> e = table[indexFor(hash, table.length)];
            e != null;
            e = e.next) {
        Object k;
        if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
            return e.value;
    }
    return null;
}

如果我们不覆盖 equals,如果我们丢失了键的引用,我们将无法从 HashMap 中检索值,例如对其他代码不可见的键引用。 读取 HashMap 的唯一方法是使用 Iterator,并通过比较值来检索准确的对:

First put: [key:Instance1Obj(144756696),  value:Person1Obj(“Baron”, “Steven”)]
Second put: [key:Instance2Obj(17488696),  value:Person2Obj(“Hewlett”, “Emily”)]

如果我们想根据帐号检索 Baron Steven 的帐号,我们不能只创建一个具有相同帐号的新对象,例如 Instance88Obj(144756696),然后调用 get 方法来检索该值。在这种情况下,我们将得到一个 null 作为结果,因为 get 方法使用基于键引用的比较,即使哈希码相同。

  • 使用 put() 方法更新值的问题:

如果 equals 未被覆盖,则通过新的实例键(但具有相同的哈希码)输入新值以更新它,将不起作用,只会添加另一对键/值:

First put: [key:Instance1Obj(144756696),  value:Person1Obj(“Baron”, “Seven”)]
Second put: [key:Instance2Obj(144756696),  value:Person2Obj(“Baron”, “Steven”)]

通常我们期望用“Steven”替换值“Seven”,但替换不会发生

public V put(K key, V value) {
    if (key == null)
        return putForNullKey(value);
    int hash = hash(key.hashCode());
    int i = indexFor(hash, table.length);
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {
        Object k;
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }

    modCount++;
    addEntry(hash, key, value, i);
    return null;
}

结论:

==> 如果你覆盖哈希码,也覆盖等于。

==>如果你覆盖equals,也覆盖hashcode。(超出这个话题:))

【问题讨论】:

  • 哈希表中的桶数不同,远小于不同哈希码的数量。这意味着,会有多个值的桶。
  • @Thomas - 你不正确。它不会“肯定”中断。事实上,从某种意义上说,它根本不会破裂。阅读答案。
  • hashCode() 没有给出桶索引,而是Map 实现使用的值作为计算桶索引的基础。

标签: java hashmap key equals hashcode


【解决方案1】:

如果你的类使用来自Objectequals 方法,那么任何幂等的hashCode 实现都可以。如果它使用来自Object 以外的某个超类的equals 方法,那么您需要确保您的hashCode 实现与其兼容。

【讨论】:

  • 是的,我们应该谨慎对待这种情况
【解决方案2】:

可以避免实现equals,或者避免实现hashcode。在这两种情况下,你都会向一个充满潜在痛苦的世界敞开心扉。毕竟请记住,哈希用于将对象分成桶,然后桶中的对象仍然需要检查是否相等,您依赖于您正在使用的集合对象以完全符合您需要的方式运行。

这些指导方针的存在是为了防止出现很多问题,没有充分的理由避免遵循这些指导方针,除非尝试变得聪明,这样才能为未来的大量问题敞开心扉。

【讨论】:

  • 是的,我同意你的观点,覆盖等于它是发生碰撞时的安全或预防措施。
  • @jamesbaron 避免哈希冲突并避免在哈希表存储桶中包含许多项目(除非您将哈希表容量设置为匹配哈希值大小,这需要 16 GB 内存在Java中......)。所以它不是“在冲突的情况下”,哈希冲突只与效率有关,与功能无关(无论值如何,哈希表都可以使用常量哈希,只是非常低效)。
【解决方案3】:

不同的哈希码导致不相等的对象 ==> 它说“导致”而不是“总是”。两个相等的对象总是返回相同的哈希码,但如果 hashcode 方法没有被覆盖,两个不相等的对象几乎不会返回相同的哈希码。

【讨论】:

    【解决方案4】:

    合同规定,如果两个对象相等,则它们应该具有相同的哈希码。但此外,如果不相等的对象应具有不同的哈希码,则哈希表效果最佳。

    如果你覆盖了默认的hashCode 方法而不覆盖默认的equals 方法,你很可能使hashcode 的行为不理想;即给出更多情况,其中 hashCode 为不相等的对象返回相同的值。这可能会在您的哈希表中给您带来更多的冲突,具体取决于您的 hashCode 覆盖的详细信息和您的应用程序使用的实例。最终结果将是性能显着下降。

    因此,虽然“反之亦然”并不是正确应用程序行为的严格要求,但该建议通常仍然有效。最好您的 equalhashCode 方法具有匹配的语义。

    【讨论】:

      【解决方案5】:

      哈希码的值空间比哈希图中的桶数要大几个数量级。因此,除非我们讨论的是专门的 Map 实现,例如 EnumMap,否则即使给定完美的哈希码,每个存储桶也总会有多个对象。

      不覆盖equals() 的对象只有在它们是完全相同的对象时才会被视为相等,而不管它们的字段值如何。因此,仅覆盖 hashCode() 是没有意义的,因为它所完成的只是轻微的性能损失(至少因为您的自定义哈希码需要更长的计算时间)。如果自定义哈希码不完美,甚至可能会造成很大的性能损失。

      每当您覆盖hashCode()equals() 时,您都会这样做,因为您希望对象的字段内容很重要。除非您同时覆盖两者,否则这不会发生。

      【讨论】:

        【解决方案6】:

        在标准 Java HashMap 中,equals总是用于确定密钥是否在存储桶中。如果hashCode 给了正确的桶,只有一个项目,但equals 对那个键返回 false,这意味着键不在那里。

        如果你不覆盖equals,它使用对象引用。如果要使用对象的值作为键(即可以有不同的对象实例具有相同的值),而不是要求完全相同的对象实例(相同的实例不相等),那么您需要编写equals,它实现了比较价值观。

        默认hashCode 与默认equals 一样有效。仅覆盖 hashCode 可能会降低性能,而不会改变行为(equals 仍然确定两个关键对象是否相等)。这样做是没有意义的。

        你没有问这个,但另外,如果你覆盖equals而不覆盖hashCode,那么你打破HashMap键,因为hashCode可能会把你带到错误的桶,而equals 将找不到密钥,因为它只检查一个桶。

        你可以做什么: 如果你能保证每个项目都有不同的hashCode,你可以写equals,它只比较哈希码。如果您在对象中缓存哈希码,那么这可以提高性能,使equals 只比较两个整数。当然还有内存开销。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2012-02-26
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2013-03-15
          • 2012-11-05
          • 1970-01-01
          相关资源
          最近更新 更多