在Java中,为什么equals()和hashCode()必须一致?

时间:2009-11-05 03:42:51

标签: java

如果我覆盖某个类的任一方法,则必须确保A.equals(B) = true如果(A.hashCode() == B.hashCode)必须为真。

有人能告诉我一个简单的例子,如果这被违反,会导致问题吗?我认为如果你使用该类作为Hashmap的键类型,它有什么关系?

6 个答案:

答案 0 :(得分:16)

不确定

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

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

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

  1. 计算蒙面哈希码;
  2. 找到合适的存储桶;
  3. 如果它为空,则找不到钥匙;
  4. 如果is不为空​​,则遍历存储桶中的所有条目,检查equals()。
  5. 查看存储桶是一个线性(O(n))操作,但它只是一个小子集。哈希码桶确定基本上是常数(O(1))。如果存储桶足够小,那么对HashMap的访问通常被描述为“接近O(1)”。

    您可以就此发表一些意见。

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

    其次,返回到原始点,如果两个对象相等(意思是。equals(b) == b.equals(a) == true)但是具有不同的哈希码,则HashMap将查找(可能)错误的桶,从而导致不可预测和不确定的行为。

答案 1 :(得分:7)

项目8:在覆盖Joshua Bloch的Effective Java的等于时始终覆盖hashCode:

  

常见的错误来源是无法覆盖hashCode方法。你必须   覆盖覆盖等于的每个类中的hashCode。不这样做会   导致违反Object.hashCode的一般合同,该合同将在   让你的班级与所有基于哈希的集合一起正常运作   tions,包括HashMap,HashSet和Hashtable。

     

这是从中复制的合同   java.lang.Object规范:

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

  •     
  • 如果两个对象根据equals(Object)方法相等,则对两个对象中的每一个调用hashCode方法必须产生相同的整数结果。

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

  •     
  
     

当您无法覆盖hashCode时违反的密钥配置是   第二个:Equal对象必须具有相同的哈希码。两个不同的   根据类的equals方法,实例在逻辑上可能相等,但是   在Object类的hashCode方法中,它们只是两个没有多少的对象   共同的。因此object的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

答案 2 :(得分:1)

这是一个小例子:

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,以便您可以找到它。

答案 3 :(得分:1)

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

答案 4 :(得分:1)

这正是因为哈希表。

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

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

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

显然,这可能很糟糕。

答案 5 :(得分:0)

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