equals() 和 hashCode() 是 Object 类的两个方法,Object 是其他所有 Java 类的父类,所以任何 Java object 都拥有这两个方法。
先看 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();
再看 Java API 文档中 Object 类中对这两个方法的说明:
public boolean equals(Object obj)
指示某个其他对象是否与此对象「相等」。
equals 方法在非空对象引用上实现相等关系:
Object 类的 equals 方法实现对象上差别可能性最大的相等关系;即,对于任何非空引用值 x 和 y,当且仅当 x 和 y 引用同一个对象时,此方法才返回 true(x == y 具有值 true)。
注意:当此方法被重写时,通常有必要重写 hashCode 方法,以维护 hashCode 方法的常规协定,该协定声明相等对象必须具有相等的哈希码。
参数:
obj - 要与之比较的引用对象。
返回:
如果此对象与 obj 参数相同,则返回 true;否则返回 false。
另请参见:
hashCode(), Hashtable
public int hashCode()
返回该对象的哈希码值。支持该方法是为哈希表提供一些优点,例如,java.util.Hashtable 提供的哈希表。
hashCode 的常规协定是:
返回:
此对象的一个哈希码值。
另请参见:
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)