在java中,为什么equals()和hashcode()必须一致?

vq8itlhq  于 2021-06-30  发布在  Java
关注(0)|答案(6)|浏览(361)

如果我重写类上的任何一个方法,它必须确保 A.equals(B) == true 那么 A.hashCode() == B.hashCode 也一定是真的。
有人能给我举一个简单的例子,如果违反了这一点,就会引起问题吗?我认为这与是否使用该类作为hashmap的键类型有关?

ct2axkht

ct2axkht1#

当然:

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的工作原理是这样的:它有一个通常称为“bucket”的可变数目。bucket的数量可以随着时间的推移而变化(随着条目的添加和删除),但它始终是2的幂。
假设一个给定的 HashMap 有16个桶。调用put()添加条目时,会计算键的hashcode(),然后根据存储桶的大小获取掩码。如果您(按位)将hashcode()与15(0x0f)相加,您将得到最后4位,等于一个介于0和15之间(含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。所有具有相同屏蔽哈希代码的条目都放入某种列表中。
因此,要确定给定的密钥是否已在Map中:
计算屏蔽哈希码;
找到合适的桶;
如果为空,则找不到密钥;
如果不是空的,则循环检查bucket中的所有条目checking equals()。
查看bucket是一个线性(o(n))操作,但它是在一个很小的子集上。hashcode bucket的确定基本上是常量(o(1))。如果bucket足够小,那么对hashmap的访问通常被描述为“接近o(1)”。
你可以对此做一些观察。
首先,如果有一堆对象都返回42作为散列码 HashMap 仍将有效,但它将作为一个昂贵的名单运作。访问将是o(n)(因为无论存储桶的数量如何,所有内容都将在同一个存储桶中)。我在面试中被问到过这个问题。
第二,回到原点,如果两个物体相等(意思是a。 equals(b) == b.equals(a) == true )但是有不同的散列码 HashMap 将去寻找(可能)错误的桶导致不可预测和未定义的行为。

qmb5sa22

qmb5sa222#

下面是一个小例子:

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.
}

因此,假设为 firstFoo99999 它会在地球上的一个特定地点结束 myFoos 哈希集。等你拿到票的时候 someFoo 你就在房间里找 myFoos hashset,它需要生成相同的hashcode以便您可以找到它。

lyfkaqu1

lyfkaqu13#

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

emeijp43

emeijp434#

这完全是因为哈希表。
由于哈希代码冲突的可能性,哈希表还需要检查标识,否则该表无法确定它是否找到了要查找的对象,或者找到了具有相同哈希代码的对象。所以每 get() 在哈希表中调用 key.equals(potentialMatch) 在返回值之前。
如果 equals() 以及 hashCode() 如果你的行为不一致,你会有非常不一致的行为。比如说两个物体, a 以及 b , a.equals(b) 返回true,但是 a.hashCode() != b.hashCode() . 插入一个,hashset将为返回false .contains(b) ,但从该集合创建的列表将返回true(因为该列表不使用哈希代码)。

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

很明显,那可能很糟糕。

odopli94

odopli945#

这在第8项中进行了讨论:重写joshua bloch有效java的equals时始终重写hashcode:
bug的一个常见来源是未能重写hashcode方法。必须在每个重写equals的类中重写hashcode。否则将违反object.hashcode的一般约定,这将阻止类与所有基于哈希的集合(包括hashmap、hashset和hashtable)一起正常工作。
以下是从java.lang.object规范复制的合同:
在应用程序执行期间,每当在同一对象上多次调用hashcode方法时,只要不修改对象的equals比较中使用的信息,hashcode方法就必须始终返回相同的整数。从一个应用程序的一个执行到同一应用程序的另一个执行,这个整数不需要保持一致。
如果根据equals(object)方法两个对象相等,那么对两个对象中的每个对象调用hashcode方法必须产生相同的整数结果。
根据equals(object)方法,如果两个对象不相等,则对这两个对象中的每一个调用hashcode方法都必须产生不同的整数结果。但是,程序员应该知道,为不相等的对象生成不同的整数结果可能会提高哈希表的性能。
当您无法重写hashcode时违反的关键规定是第二条:相等的对象必须具有相等的hashcode。根据类的equals方法,两个不同的示例在逻辑上可能相等,但是对于对象类的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,另一个equal示例用于(尝试)检索。phonenumber类未能重写hashcode导致两个相等的示例具有不相等的哈希代码,这违反了hashcode约定。因此,get方法在一个不同于put方法存储电话号码的哈希桶中查找电话号码。解决此问题非常简单,只需为phonenumber类提供适当的hashcode方法。[…]
完整内容见第3章。

u3r8eeie

u3r8eeie6#

像hashset这样的容器依赖于hash函数来确定将它放在何处,以及在请求时从何处获取它。如果 A.equals(B) ,则哈希集希望 B . 如果你把 A 有价值的 V ,然后向上看 B ,你应该期望 V 回来(自从你说过 A.equals(B) ). 但是如果a.hashcode()!=b、 hashcode(),则hashset可能找不到放置它的位置。

相关问题