【问题标题】:In Java, why must equals() and hashCode() be consistent?在 Java 中,为什么 equals() 和 hashCode() 必须一致?
【发布时间】:2009-11-05 03:42:50
【问题描述】:

如果我在一个类上重写任一方法,它必须确保如果A.equals(B) == trueA.hashCode() == B.hashCode 也必须为真。

谁能给我一个简单的例子,如果违反了,会导致问题吗?我认为这与您使用该类作为 Hashmap 的键类型有关吗?

【问题讨论】:

  • 这不是答案,但请注意 hashCode() 的全部目的是提供一个任何相等对象都必须共享的数字。如果不是因为那个属性,它就没有存在的理由。

标签: java


【解决方案1】:

当然:

public class Test {
  private final int m, n;

  public Test(int m, int n) {
    this.m = m;
    this.n = n;
  }

  public int hashCode() { return n * m; }

  public boolean equals(Object ob) {
    if (ob.getClass() != Test.class) return false;
    Test other = (Test)ob;
    return m == other.m;
  }
}

与:

Set<Test> set = new HashSet<Test>();
set.put(new Test(3,4));
boolean b = set.contains(new Test(3, 10)); // false

从技术上讲,这应该是正确的,因为在这两种情况下 m == 3。

一般来说,HashMap 的工作方式是这样的:它具有可变数量的通常称为“桶”的东西。桶的数量可以随时间变化(随着条目的添加和删除),但它始终是 2 的幂。

假设给定的HashMap 有 16 个桶。当您调用 put() 添加条目时,将计算键的 hashCode(),然后根据存储桶的大小获取掩码。如果你(按位)将 hashCode() 与 15 (0x0F) 相加,你将得到最后 4 位,等于 0 到 15 之间的数字:

int factor = 4;
int buckets = 1 << (factor-1) - 1; // 16
int mask = buckets - 1; // 15
int code = key.hashCode();
int dest = code & mask; // a number from 0 to 15 inclusive

现在,如果该存储桶中已经有一个条目,那么您就有了所谓的冲突。有多种方法可以解决这个问题,但 HashMap 使用的一种(可能是最常见的一种)是 bucketing。所有具有相同掩码 hashCode 的条目都放在某种列表中。

所以要查找给定的键是否已经在地图中:

  1. 计算掩码哈希码;
  2. 找到合适的桶;
  3. 如果为空,则找不到密钥;
  4. 如果 is 不为空,则循环遍历存储桶中的所有条目,检查 equals()。

查看存储桶是一个线性 (O(n)) 操作,但它位于一个小子集上。哈希码桶的确定基本上是恒定的 (O(1))。如果桶足够小,那么对 HashMap 的访问通常被描述为“接近 O(1)”。

您可以对此进行一些观察。

首先,如果你有一堆对象都返回 42 作为它们的哈希码,HashMap 仍然可以工作,但它将作为一个昂贵的列表运行。访问将是 O(n)(因为无论存储桶的数量如何,所有东西都将在同一个存储桶中)。我实际上在一次采访中被问到这个问题。

其次,回到你原来的观点,如果两个对象相等(意味着 a.equals(b) == b.equals(a) == true)但具有不同的哈希码,那么 HashMap 将进入(可能)错误的存储桶,从而导致不可预测和未定义的行为.

【讨论】:

  • 好的。那么当您调用 set.contains(new Test(3,10)) 时,幕后究竟发生了什么?
  • +1。您的示例根本不是人为的;在 JPA 中处理持久集时,这是一个非常现实的问题。人们倾向于根据代理键编写 equals() / hashCode() 并想知道为什么他们的集合元素在保存后突然消失了。
【解决方案2】:

这在 Joshua Bloch 的 Effective Java 的 Item 8: Always override hashCode when you override equals 中进行了讨论:

错误的常见来源是未能覆盖 hashCode 方法。你必须 在每个覆盖equals的类中覆盖hashCode。不这样做会 导致违反 Object.hashCode 的一般合同,这将预先 让你的类与所有基于散列的集合一起正常运行 HashMap、HashSet、Hashtable等。

这是合同,从 java.lang.Object 规范:

  • 在应用程序执行期间,只要在同一个对象上多次调用它,hashCode 方法必须始终返回相同的整数,前提是没有修改对象上的 equals 比较中使用的信息。该整数不需要在应用程序的一次执行与同一应用程序的另一次执行之间保持一致。

  • 如果两个对象根据equals(Object)方法相等,那么对这两个对象分别调用hashCode方法必须产生相同的整数结果。

  • 如果两个对象根据equals(Object) 方法不相等,则不要求对两个对象中的每一个调用hashCode 方法必须产生不同的整数结果。但是,程序员应该意识到,为不相等的对象生成不同的整数结果可能会提高哈希表的性能。

未能覆盖 hashCode 时违反的关键条款是 第二个:相等的对象必须有相等的哈希码。 两个不同的 根据类的 equals 方法,实例可能在逻辑上相等,但是 Object 类的 hashCode 方法,它们只是两个对象而已 共同点。因此对象的 hashCode 方法返回两个看似随机的 数字而不是合同要求的两个相等的数字。

例如,考虑以下简单的 PhoneNumber 类,其 equals 方法是根据第 7 项中的配方构造的:

public final class PhoneNumber {
     private final short areaCode;
     private final short exchange;
     private final short extension;

     public PhoneNumber(int areaCode, int exchange,
                           int extension) {
         rangeCheck(areaCode,   999, "area code");
         rangeCheck(exchange,   999, "exchange");
         rangeCheck(extension, 9999, "extension");

         this.areaCode = (short) areaCode;
         this.exchange = (short) exchange;
         this.extension = (short) extension;
     }

     private static void rangeCheck(int arg, int max,
                                 String name) {
         if (arg < 0 || arg > max)
             throw new IllegalArgumentException(name +": " + arg);
     }

     public boolean equals(Object o) {
         if (o == this)
             return true;
         if (!(o instanceof PhoneNumber))
             return false;
         PhoneNumber pn = (PhoneNumber)o;
         return pn.extension == extension &&
                pn.exchange == exchange &&
                pn.areaCode == areaCode;
     }

     // No hashCode method!
    ... // Remainder omitted
}

假设您尝试使用此类 使用 HashMap:

Map m = new HashMap();
m.put(new PhoneNumber(408, 867, 5309), "Jenny");

此时,您可能会期望 m.get(new PhoneNumber(408 , 867, 5309)) 返回"Jenny",但它 返回null。请注意,两个 PhoneNumber 实例是 涉及:一个用于插入 进入 HashMap,然后第二个,相等, 实例用于(尝试) 恢复。 PhoneNumber 类的 未能覆盖 hashCode 原因 两个相等的实例 不相等的哈希码,违反 hashCode 合约。因此 get 方法查找电话号码 在不同的哈希桶中 一个由 put 存储的 方法。解决这个问题是 就像提供适当的 hashCode 一样简单 PhoneNumber 类的方法。 [...]

查看Chapter 3了解完整内容。

【讨论】:

  • 它返回 null 因为PhoneNumberPhoneNumberhashCode() 的返回值与get 中的不同,因此查找(get)不会找到要迭代的正确的项目桶,在每个项目上测试equals。因为get 迭代了错误的桶,所以它不会找到put 对象并返回null
  • @Saobi - 因为从 Object 继承的 hashcode 的实现返回一个身份 hashcode。尽管equals 方法表明它们是相同的,但这两个 PhoneNumber 实例很可能具有不同的身份哈希码。所以get操作在HashMap中找错了bucket,什么也没找到,返回null。
  • @StephenC ...因此,get 方法在与 put 方法存储电话号码不同的哈希存储桶中查找电话号码...即使上面不存在哈希码覆盖,默认hascode impl。将用于 put() 和 get() 并生成相同的哈希并找到相同的存储桶,不是吗?这与equals()有什么关系?我很困惑。
【解决方案3】:

像 HashSet 这样的容器依赖于哈希函数来确定将它放在哪里,以及在被要求时从哪里获取它。如果A.equals(B),那么HashSet 期望A 与B 在同一个位置。如果您将A 放入值V,然后查找B,您应该期望得到V(因为您已经说过A.equals(B))。但是如果 A.hashcode() != B.hashcode(),那么 hashset 可能找不到你放的地方。

【讨论】:

    【解决方案4】:

    这是一个小例子:

    Set<Foo> myFoos = new HashSet<Foo>(); 
    Foo firstFoo = new Foo(123,"Alpha");
    myFoos.add(firstFoo);
    
    // later in the processing you get another Foo from somewhere
    Foo someFoo = //use imagination here...; 
    // maybe you get it from a database... and it's equal to Foo(123,"Alpha)
    
    if (myFoos.contains(someFoo)) {
       // maybe you win a million bucks.
    }
    

    因此,假设为 firstFoo 创建的 hashCode 是 99999,它在 myFoos HashSet 中的特定位置结束。稍后当你得到someFoo,你在myFoos HashSet 中查找它时,它需要生成相同的hashCode 以便你找到它。

    【讨论】:

      【解决方案5】:

      正是因为哈希表。

      由于哈希码冲突的可能性,哈希表也需要检查身份,否则该表无法确定它是否找到了它正在寻找的对象,或者具有相同哈希码的对象。所以哈希表中的每个get() 在返回值之前都会调用key.equals(potentialMatch)

      如果equals()hashCode() 不一致,您会得到非常不一致的行为。假设有两个对象,aba.equals(b) 返回 true,但 a.hashCode() != b.hashCode()。对于.contains(b),插入 a 和 HashSet 将返回 false,但从该集合创建的 List 将返回 true(因为该列表不使用哈希码)。

      HashSet set = new HashSet();
      set.add(a);
      set.contains(b); // false
      new ArrayList(set).contains(b); // true
      

      显然,这可能很糟糕。

      【讨论】:

        【解决方案6】:

        这背后的想法是,如果两个对象的所有字段都具有相等的值,则它们是“相等的”。如果所有字段的值都相等,那么这两个对象应该具有相同的哈希值。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2019-01-22
          相关资源
          最近更新 更多