java 中的 equals() 以及 hashcode() 的使用

核心

equals()相等的两个对象,hashcode()一定相等;
equals()不相等的两个对象,却并不能证明他们的hashcode()不相等。

换句话说

hashcode()不等,一定能推出equals()也不等;
hashcode()相等,equals()可能相等,也可能不等。

分析

看到上面的话,肯定有人会提出质疑,肯定存在“equals()相等的两个对象,hashcode()不相等”,比如以下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class Point {
private int x;
private int y;
public Point(int x, int y) {
this.x = x;
this.y = x;
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null) return false;
if (obj instanceof Point) {
Point p = (Point) obj;
if (this.x == p.x && this.y == p.y) return true;
}
return false;
}
}
public class Test {
public static void main(String[] args) {
Point p1 = new Point(3, 3);
Point p2 = new Point(3, 3);
System.out.println(p1.equals(p2));
System.out.println(p1.hashCode());
System.out.println(p2.hashCode());
}
}

上面的代码输出的结果应该类似于以下形式

true
569329915
1688234020

可以看到,equals()值相等,hashcode()值并不相等,怎么回事呢?实际上,上面的代码在大多数情况下都可以正确地工作,但请注意,我说的是“大多数情况下”,比如下面即是“小部分情况”

1
2
3
4
5
6
7
8
9
10
public class Test {
public static void main(String[] args) {
Point p1 = new Point(3, 3);
Point p2 = new Point(3, 3);
Set<Point> s = new HashSet<Point>();
s.add(p1);
s.add(p2);
System.out.println(s.size());
}
}

上面的代码将会输出结果2,这肯定与我们预期的结果不同。我们肯定是希望往HashSet中加入元素的时候忽略“相等(euqals())”的元素。是的,p1p2确实是相等的,但是在往HashSet里面添加元素的时候并不是仅仅由equals()决定的,它首先会比较元素hashcode()值,如果元素的hashcode不一样,则会将元素直接加入到HashSet中去,并不会使用equals()方法判定。而如果hashcode值相等的时候才会使用equals()进行比较。

肯定有人会有疑问,既然这样,为什么还要设计hashcode()方法,一个equals()不就足够了吗?

是的,只要一个equals()方法肯定可以保证代码的执行,但是这样做却忽略了一个重要指标–效率。hashcode()的存在可以大大地提升效率。试想,如果仅使用equals()方法,在往一个HashSet中添加元素的时候,需要将这个元素与HashSet中的每个元素进行euqals()比较,时间为O(N),而如果使用hashcode()方法,根据该元素的hashcode值,可以直接定位到它的位置,如果该位置没有元素,则直接插入(equals()肯定不相等),否则,再使用equals()对比,相等则丢弃,不必插入,不相等则可以使用其他方法(如向后移动一位等),时间复杂度降至O(1)

因此,重写equals()方法总是要重写hashcode(),请务必遵守。
为上面的Point类添加以下方法即可:

1
2
3
4
5
6
7
@Override
public int hashCode() {
int result = 1;
result = 31 * result + x;
result = 31 * result + y;
return result;
}

此时再执行上面的main方法,将会输出1,正是理想的结果。

当然,你甚至可以写成如下形式的hashcode()方法:

1
2
3
4
@Override
public int hashCode() {
return 11;
}

这样,结果仍然正确,只是效率又会下降很多。

equals()的写法

关于equals()的重写,需要注意以下几点:

  1. 反性,即x.equals(x)永远返回true,当然x != null
  2. 对称性,即x.equals(y) == y.equals(x)
  3. 传递性,即x.equals(y) == truey.equals(z) == true时一定有x.equals(z) == true
  4. 一致性,即x.equals(y)的结果只跟xy相关,亦即xy不变,x.equals(y)返回值不变;
  5. x.equals(null)一定返回null

可参见《Effictive Java》pp.28-38,平时使用可模仿上文Point的写法。

hashcode()的写法

《Effictive Java》p.41给出了一个通用的写法,这里仅给出一个简单的写法,

1
2
3
4
result = 17;
result = result * 37 + property1.hashcode;
result = result * 37 + property2.hashcode;
result = result * 37 + property3.hashcode;

想偷懒或想更好的实现还可以使用Apache Commons Lang包来重写hashCode()equals()方法。