为什么重写equals方法时必须重写hashCode方法

Java中的hash值主要是用来在散列存储结构中确定对象的存储地址的,提高对象的查询效率。

Java设计的顶级父类Object类中,有两个方法很特殊,它们分别是equals方法与hashCode方法。——一旦重写了equals方法,就一定要重写hashCode方法。以下是Object的源码:

public class Object {

 /* 
  * Note that it is generally necessary to override the {@code hashCode}
  * method whenever this method is overridden, so as to maintain the
  * general contract for the {@code hashCode} method, which states
  * that equal objects must have equal hash codes.
  */
  public boolean equals(Object obj) {
    return (this == obj);
  }

 /*
  * The general contract of {@code hashCode} is:
  * 
    *
  • Whenever it is invoked on the same object more than once during * an execution of a Java application, the {@code hashCode} method * must consistently return the same integer, provided no information * used in {@code equals} comparisons on the object is modified. * This integer need not remain consistent from one execution of an * application to another execution of the same application. *
  • If two objects are equal according to the {@code equals(Object)} * method, then calling the {@code hashCode} method on each of * the two objects must produce the same integer result. *
  • It is not required that if two objects are unequal * according to the {@link java.lang.Object#equals(java.lang.Object)} * method, then calling the {@code hashCode} method on each of the * two objects must produce distinct integer results. However, the * programmer should be aware that producing distinct integer results * for unequal objects may improve the performance of hash tables. *
*

* As much as is reasonably practical, the hashCode method defined by * class {@code Object} does return distinct integers for distinct * objects. (This is typically implemented by converting the internal * address of the object into an integer, but this implementation * technique is not required by the * Java™ programming language.) * */ public int hashCode() { return identityHashCode(this); } /* package-private */ static int identityHashCode(Object obj) { int lockWord = obj.shadow$_monitor_; final int lockWordStateMask = 0xC0000000; // Top 2 bits. final int lockWordStateHash = 0x80000000; // Top 2 bits are value 2 (kStateHash). final int lockWordHashMask = 0x0FFFFFFF; // Low 28 bits. if ((lockWord & lockWordStateMask) == lockWordStateHash) { return lockWord & lockWordHashMask; } return identityHashCodeNative(obj); } }

如果一个类没有重写equals(Object obj)方法,则等价于通过==比较两个对象,即比较的是对象在内存中的空间地址是否相等;如果重写了equals(Object obj)方法,则根据重写的方法内容去比较相等,返回true则相等,false则不相等。

equals方法注释中的大致意思是:当我们将equals方法重写后有必要将hashCode方法也重写,这样做才能保证不违背hashCode方法中“相同对象必须有相同哈希值”的约定。

hashCode方法本质就是一个哈希函数,Object类的作者在注释的最后一段的括号中写道:将对象的地址值映为integer类型的哈希值。

提到hashCode,就会想到哈希表,将某一对象的值映射到表中的某个位置,从而达到以O(1)的时间复杂度来查询该对象的值。hashCode是一个native方法,哈希值的计算利用的内存地址。哈希表在一定程度上也可以起到判重的作用,但也可能存储哈希冲突,即使是两个不同的对象,它们的哈希值也可能是相同的。因此,虽然哈希表具有优越的查询性能,但也可能存在哈希冲突。

hashCode方法注释中列了个列表,列表中有三条注释,当前需要理解的大致意思如下:

  • 一个对象多次调用它的hashCode方法,应当返回相同的integer(哈希值);
  • 两个对象如果通过equals方法判定为相等,那么就应当返回相同integer
  • 两个地址值不相等的对象调用hashCode方法不要求返回不相等的integer,但是要求拥有两个不相等integer的对象必须是不同对象。

为什么重写equals方法时必须重写hashCode方法_第1张图片

图中存在两种独立的情况:

  • 相同的对象必然导致相同的哈希值;
  • 不同的哈希值必然是由不同对象导致的;

因此,equals方法与hashCode方法根本就是配套使用的。对于任何一个对象,不论是使用继承自Objectequals方法还是重写equals方法。hashCode方法实际上必须要完成的一件事情就是,为该equals方法认定为相同的对象返回相同的哈希值。如果只重写equals方法没有重写hashCode方法,就会导致``equals`认定相同的对象却拥有不同的哈希值。

Object类中的equals方法区分两个对象的做法是比较地址值,即使用==。如果根据业务需求改写了equals方法的实现,那么也应当同时改写hashCode方法的实现。否则hashCode方法依然返回的是依据Object类中的依据地址值得到的integer哈希值。

代入到具体的例子 –String类,在String类中,equals方法经过重写,具体实现源码如下:

public final class String implements java.io.Serializable, Comparable<String>, CharSequence {

  public boolean equals(Object anObject) {
    if (this == anObject) {
      return true;
    }
    if (anObject instanceof String) {
      String anotherString = (String) anObject;
      int n = length();
      if (n == anotherString.length()) {
        int i = 0;
        while (n-- != 0) {
          if (charAt(i) != anotherString.charAt(i))
            return false;
          i++;
        }
        return true;
      }
    }
    return false;
  }

}

通过源码可以看到,String对象在调用equals方法比较另一个对象时,除了认定相同地址值的两个对象相等以外,还认定对应着的每个字符都相等的两个String对象也相等,即使这两个String对象的地址值不同(即属于两个对象)。

String类中对equals方法进行重写扩充了,但是如果此时我们不将hashCode方法也进行重写,那么String类调用的就是来自顶级父类Obejct类中的hashCode方法。即,对于两个字符串对象,使用它们各自的地址值映射为哈希值。 也就是会出现如下情形:

为什么重写equals方法时必须重写hashCode方法_第2张图片

也就是说,String类中的equals方法认定为相等的两个对象拥有两个不同的哈希值——因为它们的地址值不同。

为什么重写equals方法就得重写hashCode方法?—— 因为必须保证重写后的equals方法认定相同的两个对象拥有相同的哈希值。同时我们也得出了——hashCode方法的重写原则就是保证equals方法认定为相同的两个对象拥有相同的哈希值。

equals里一般比较的比较全面比较复杂,这样效率就比较低,而利用hashCode进行对比,则只要生成一个hash值进行比较就可以了,效率很高,那么既然hashCode效率这么高为什么还要使用equals进行比较呢?

因为hashCode并不是完全可靠,有时候不同的对象生成的hashcode也会一样(hash冲突),所以hashCode只能说是大部分时候可靠,并不是绝对可靠。 所以可以得出:

  • equals相等的两个对象,它们的hashCode肯定相等,也就是用equals对比是绝对可靠的;
  • hashCode相等的两个对象,它们的equals不一定相等,也就是hashCode不是绝对可靠的;

所有对于需要大量并且快速的对比的话如果都用equals去做显然效率太低,解决方式是,每当需要对比的时候, hashCode去对比,这就用到了哈希表,能够快速的地位到对象的存储位置,如果hashCode不一样,则表示这两个对象肯定不相等(也就是不必再用equals去再对比了),如果hashCode相同,此时再对比它们的 equals,如果equals也相同,则表示这两个对象是真的相同了。

查看字符串的hashCode

String str = new String("abc");
System.out.println(str.hashCode()); // 96354
str += "a";
System.out.println(str.hashCode()); // 2987071
str += "b";
System.out.println(str.hashCode()); // 92599299
String str1 = "abcab";
System.out.println(str.hashCode() + " " + str1.hashCode()); // 92599299 92599299

String str2 = "ab";
String str3 = new String("ab");
System.out.println(str2.hashCode() + " " + str3.hashCode()); // 3105 3105

以下是String.hashCode的源码:

public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
  
  private int hash; // Default to 0
  
   /**
     * Returns a hash code for this string. The hash code for a
     * {@code String} object is computed as
     * 
     * s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
     * 
* using {@code int} arithmetic, where {@code s[i]} is the * ith character of the string, {@code n} is the length of * the string, and {@code ^} indicates exponentiation. * (The hash value of the empty string is zero.) * * @return a hash code value for this object. */
public int hashCode() { int h = hash; if (h == 0 && value.length > 0) { char val[] = value; for (int i = 0; i < value.length; i++) { h = 31 * h + val[i]; } hash = h; } return h; } }

由上面的代码可知,String类的hashCode的值为s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1],其中s是字符串对应的char数组,所以字符串内容一样的String对象,调用hasCode()返回值是一样的。但是反过来,当hashCode返回的值一样时,其字符串内容不一定一样,因为上面的方法计算hash时可能会发生位数溢出。

参考

https://zhuanlan.zhihu.com/p/50206657
https://blog.csdn.net/weixin_64735186/article/details/122727751

你可能感兴趣的:(java,哈希算法,散列表)