IT Notes‎ > ‎Java‎ > ‎Java Language‎ > ‎

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 是否相同? equals() 的值
 true true/false
 false true/false
hashCode 的值是否相等,可以和 equals 无关。

对于 hashCode 方法说明中的「常规协定」第三点,现实中可能存在这样的情况:equals 函数返回 false,但 hashCode 值相等,这点在上表中也可以看出。实验显示,对于相同类型的对象,如果 hashCode 相同,但 equals 返回 false,还是可以存到一个集合中去的,说明它们被视为不同的元素。而如果此时强制把 equals 也改成相同的话,那么就只能存一个了。下表列出了 HashSet 中存放同一类型的两个对象的情况:
 hashCode 是否相同? equals() 的值 HashSet 中最终存了几个对象? 合理否?
 false false 2 合理
 false true 2 不合理,如果是这样,数据库中会出现重复记录
 true true 1 合理,集合存放元素,会根据 equals() 函数和 hashCode() 的返回值判断是否为同一个元素,如果两者都相等,则为同一元素,否则为不同的元素。
 true false 2 合理,但参 hashCode() 「常规协定」第三点,为不相等对象生成不同的 hashCode 可以提高访问效率

上表的结论,可以通过下面的代码验证,不同情况,可以通过打开或关闭注释来完成。
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)

Comments