Object 的方法:hashCode, equals

equals() 和 hashCode() 是 Object 类的两个方法,Object 是其他所有 Java 类的父类,所以任何 Java object 都拥有这两个方法。

Object.hashCode(), equals()

先看 Object 类的源码,如下:

/* 按这里的定义,当两个参与比较的对象引用同一个对象时,函数的返回值才为 true */
public boolean equals(Object obj) {
        return (this == obj);
}

如是,下面代码片段的输出结果是什么呢?

        HelloWorld001 a = new HelloWorld001();
        HelloWorld001 b = new HelloWorld001();
        System.out.println(a.equals(b));
        System.out.println(a == b);

这是输出结果:

false
false

解析:如果 equals 方法没有重写,那么比较的内容还是对象的引用,和==是等价的。==返回「是否为同一个对象」的布尔值。有关双等号==的说明,参这里(15.21 Equality Operators)。

/* 从关键字 native 可知 hashCode 函数是一个依赖 JVM 实现的函数 */
public native int hashCode();

hashCode 值和 equals() 值的各种组合,以及在集合对象中的各种现象

再看 Java API 文档中 Object 类中对这两个方法的说明:

equals

public boolean equals(Object obj)

指示某个其他对象是否与此对象「相等」。

equals 方法在非空对象引用上实现相等关系:

  • 自反性:对于任何非空引用值 x,x.equals(x) 都应返回 true。
  • 对称性:对于任何非空引用值 x 和 y,当且仅当 y.equals(x) 返回 true 时,x.equals(y) 才应返回 true。
  • 传递性:对于任何非空引用值 x、y 和 z,如果 x.equals(y) 返回 true,并且 y.equals(z) 返回 true,那么 x.equals(z) 应返回 true。
  • 一致性:对于任何非空引用值 x 和 y,多次调用 x.equals(y) 始终返回 true 或始终返回 false,前提是对象上 equals 比较中所用的信息没有被修改。
  • 对于任何非空引用值 x,x.equals(null) 都应返回 false。

Object 类的 equals 方法实现对象上差别可能性最大的相等关系;即,对于任何非空引用值 x 和 y,当且仅当 x 和 y 引用同一个对象时,此方法才返回 true(x == y 具有值 true)。

注意:当此方法被重写时,通常有必要重写 hashCode 方法,以维护 hashCode 方法的常规协定,该协定声明相等对象必须具有相等的哈希码。

参数

obj - 要与之比较的引用对象。

返回

如果此对象与 obj 参数相同,则返回 true;否则返回 false。

另请参见

hashCode(), Hashtable

hashCode

public int hashCode()

返回该对象的哈希码值。支持该方法是为哈希表提供一些优点,例如,java.util.Hashtable 提供的哈希表。

hashCode 的常规协定是:

  • 在 Java 应用程序执行期间,在同一对象上多次调用 hashCode 方法时,必须一致地返回相同的整数,前提是对象上 equals 比较中所用的信息没有被修改。从某一应用程序的一次执行到同一应用程序的另一次执行,该整数无需保持一致。
  • 如果根据 equals(Object) 方法,两个对象是相等的,那么在两个对象中的每个对象上调用 hashCode 方法都必须生成相同的整数结果。
  • 以下情况不是必需的:如果根据 equals(java.lang.Object) 方法,两个对象不相等,那么在两个对象中的任一对象上调用 hashCode 方法必定会生成不同的整数结果。但是,程序员应该知道,为不相等的对象生成不同整数结果可以提高哈希表的性能。
  • 实际上,由 Object 类定义的 hashCode 方法确实会针对不同的对象返回不同的整数。(这一般是通过将该对象的内部地址转换成一个整数来实现的,但是 JavaTM 编程语言不需要这种实现技巧。)

返回

此对象的一个哈希码值。

另请参见

equals(java.lang.Object), Hashtable

以上说明文字值得细读,其实细读之後,一个常见问题就得解了:为什么重写 equals 函数时一般都要重写 hashCode 函数呢?原因在 hashCode 方法说明中的「常规协定」第二点,也已用赭红色标出。之所以做出这样的协定,是为了逻辑上合理性:集合里不存在重复的元素——两个「相等」的元素(这里是对象<Object实例>) 应该视为同一个元素。在 Java 中,集合区别元素依赖于 hashCode,那么「相等」的元素既然视为同一元素,就应该有相同的 hashCode,否则「相等」的元素会因为不同的 hashCode 成为集合中不同的元素。

注意到 hashCode 方法说明中的「常规协定」第二点中黑体标出的「必须」二字,不是「必定」。所以你还是可以写出这样的违反协定的代码:equals 比较结果为真,但 hashCode 不同。虽然这样的代码没什么逻辑上的合理性,事实上,我们可以写出这几种情况的代码:

hashCode 是否相同?

true

false

equals() 的值

true/false

true/false

hashCode 的值是否相等,可以和 equals 无关。

对于 hashCode 方法说明中的「常规协定」第三点,现实中可能存在这样的情况:equals 函数返回 false,但 hashCode 值相等,这点在上表中也可以看出。实验显示,对于相同类型的对象,如果 hashCode 相同,但 equals 返回 false,还是可以存到一个集合中去的,说明它们被视为不同的元素。而如果此时强制把 equals 也改成相同的话,那么就只能存一个了。下表列出了 HashSet 中存放同一类型的两个对象的情况:

上表的结论,可以通过下面的代码验证,不同情况,可以通过打开或关闭注释来完成。

import java.util.HashSet;
import java.util.Set;
public class HelloWorld010 {
    public static void main(String[] args) {
        HelloTest h1 = new HelloTest();
        HelloTest h2 = new HelloTest();
       
        HelloTest h3 = h2;
       
        h1.setHello("hello");
        h2.setHello("world");
       
        Set<HelloTest> set = new HashSet<HelloTest>();
        set.add(h1);
        set.add(h2);
        set.add(h3);
        System.out.println(set.size());
    }
}
class HelloTest {
    private String hello;
    public String getHello() {
        return hello;
    }
    public void setHello(String hello) {
        this.hello = hello;
    }
//    @Override
//    public int hashCode() {
//        return 1;
//    }
    @Override
    public boolean equals(Object obj) {
        return true;
    }
}

如上面的代码,运行後输出2,但如果取消对 HelloTest.hashCode() 的注释,就会输出1.

如何重写

在实际应用中,很多地方需要根据应用逻辑来重定义这两个方法,对于 equals 和 hashCode 的重写,《Java:重写equals()和hashCode() 》 (by zhangjunhd)写的非常清楚,摘录如下:

1. 设计equals()

[1]使用instanceof操作符检查「实参是否为正确的类型」。

[2]对于类中的每一个「关键域」,检查实参中的域与当前对象中对应的域值。

[2.1]对于非float和double类型的原语类型域,使用==比较;

[2.2]对于对象引用域,递归调用equals方法;

[2.3]对于float域,使用Float.floatToIntBits(afloat)转换为int,再使用==比较;

[2.4]对于double域,使用Double.doubleToLongBits(adouble) 转换为int,再使用==比较;

[2.5]对于数组域,调用Arrays.equals方法。

2. 设计hashCode()

[1]把某个非零常数值,例如17,保存在int变量result中;

[2]对于对象中每一个关键域f(指equals方法中考虑的每一个域):

[2.1]boolean型,计算(f ? 0 : 1);

[2.2]byte,char,short型,计算(int);

[2.3]long型,计算(int) (f ^ (f>>>32));

[2.4]float型,计算Float.floatToIntBits(afloat);

[2.5]double型,计算Double.doubleToLongBits(adouble)得到一个long,再执行[2.3];

[2.6]对象引用,递归调用它的hashCode方法;

[2.7]数组域,对其中每个元素调用它的hashCode方法。

[3]将上面计算得到的散列码保存到int变量c,然後执行 result = 37 * result + c;

[4]返回result。

3. 最简单的办法

可直接用 Eclipse 自动生成。鼠标放到某个类里,右键,选择 Source -> Generate hashCode() and equals()...

其他

对于 native hashCode 的实现,是依赖于具体的 JRE 环境的,暂时还没能深入了解。日後努力!

一个很好很好的讨论:《[求助]为什么重写equals方法,一定要重写HashCode方法?》 (CSDN)