今天又见到一个覆写equals方法的错误,然后想起曾经看过的一篇文章对java中的equals方法进行了详细的介绍,原文见 http://www.artima.com/lejava/articles/equality.html,本来打算全文翻译一下的,但是觉得怕自己的翻译能力有限反而会误人子弟,所以就写按照他的思路写下大体的内容。
equals方法是在Object类中定义的,可以在子类中覆盖这个方法的实现,主要用来判断两个对象是否相等。但是在实际的编码中书写一个正确的
equals方法是很困难的,甚至说绝大部分的equals方法都是有问题的。主要的问题集中于一下四类:
1) 方法签名错误
2) 改写equals方法时没有同时改写hashcode方法
3) 基于易变字段定义equals方法
4) 定义的equals方法不是一个等价关系
下面挨个介绍一下这四种错误
例子主要基于下面这个类
public class Point {
private final int x;
private final int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() {
return x;
}
public int getY() {
return y;
}
// ...
}
这个类很简单,就不介绍了。
错误一:方法签名错误
首先看下面的equals方法实现
// An utterly wrong definition of equals
public boolean equals(Point other)
{
return (this.getX() == other.getX() && this.getY() == other.getY());
}
这个方法粗看起来没有什么问题,输入下面的验证代码也是正确的
Point p1 = new Point(1, 2);
Point p2 = new Point(1, 2);
Point q = new Point(2, 3);
System.out.println(p1.equals(p2)); // prints true
System.out.println(p1.equals(q)); // prints false
但是如果你试着将Point对象放入集合中问题就出现了。
import java.util.HashSet;
HashSet<Point> coll = new HashSet<Point>();
coll.add(p1);
System.out.println(coll.contains(p2)); // prints false
明明我们已经往coll中放入了p1,而p2.equals(p1),那为什么显示没有包含p2呢?为了弄清楚原因先看另外一个例子
Object p2a = p2;
System.out.println(p1.equals(p2a)); // prints false
p2a和p2指向同一个对象,但是比较的结果却是截然不同的。原因就在于我们编写的equals方法并没有覆盖Object类的equals方法,Object类的equals方法的方法签名为
public boolean equals(Object other)
但是我们的方法中参数类型确实Point,这样的话其实我们是重载了equals方法,而且我们知道对于重载的方法是静态绑定的,所以如果参数的静态类型是Point调用的就是我们自己写得方法,如果不是Point将会调用Object类中的方法。所以 p1.equals(p2a)也就是显然的了。而对于coll来说,要注意到java对泛型的处理其实是转化为Object类型来处理的,也就是俗称的“解语法糖”。
知道了这些,我们将equals方法改写为
// A better definition, but still not perfect
@Override public boolean equals(Object other) {
boolean result = false;
if (other instanceof Point) {
Point that = (Point) other;
result = (this.getX() == that.getX() && this.getY() == that.getY());
}
return result;
}
上面的equals方法才是真正覆盖了Object的equals方法,其实只要在方法头上写上注解@override就可以杜绝这种错误的发生。
错误二: 改写equals方法时没有同时改写hashcode方法
上面改写的equals方法可以解决如p2a中的那种错误,但却还有很多问题。如下例
Point p1 = new Point(1, 2);
Point p2 = new Point(1, 2);
HashSet<Point> coll = new HashSet<Point>();
coll.add(p1);
System.out.println(coll.contains(p2)); // prints false (probably)
这里输出false的原因就是因为没有重写hashcode方法。Hashset首先会根据对象的hashcode值将对象放入一个bucket中,只有位于同一个bucket的对象比较时才有可能返回true,不在同一个bucket的对象肯定是不相等的。我想这个错误是比较容易解决的,只要重写hasncode方法就好了,但是必须保证如果两个对象通过equals方法返回是相等的,必须使得它们的hashcode值是相等的。下面是一个可能的hashcode实现,当然hashcode值得计算方法可以多种多样。
public class Point {
private final int x;
private final int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() {
return x;
}
public int getY() {
return y;
}
@Override public boolean equals(Object other) {
boolean result = false;
if (other instanceof Point) {
Point that = (Point) other;
result = (this.getX() == that.getX() && this.getY() == that.getY());
}
return result;
}
@Override public int hashCode() {
return (41 * (41 + getX()) + getY());
}
}
错误三: 基于易变字段定义equals方法
首先将我们的类修改一下
public class Point {
private int x;
private int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() {
return x;
}
public int getY() {
return y;
}
public void setX(int x) { // Problematic
this.x = x;
}
public void setY(int y) {
this.y = y;
}
@Override public boolean equals(Object other) {
boolean result = false;
if (other instanceof Point) {
Point that = (Point) other;
result = (this.getX() == that.getX() && this.getY() == that.getY());
}
return result;
}
@Override public int hashCode() {
return (41 * (41 + getX()) + getY());
}
}
改动的地方只是把x,y前面的final去掉了,增加了set和get方法。这将造成下面很诡异的错误
Point p = new Point(1, 2);
HashSet<Point> coll = new HashSet<Point>();
coll.add(p);
System.out.println(coll.contains(p)); // prints true
p.setX(p.getX() + 1);
System.out.println(coll.contains(p)); // prints false (probably)
第一个输出语句输出true是显然的,为什么第二个输出语句可能会输出false呢?难道p还能跑了?那我们测试一下p是否还在coll中
Iterator<Point> it = coll.iterator();
boolean containedP = false;
while (it.hasNext()) {
Point nextP = it.next();
if (nextP.equals(p)) {
containedP = true;
break;
}
}
System.out.println(containedP); // prints true
结果显示p还在coll中,那为什么上面会输出false呢?其实是因为当p的x值改变之后,hashcode值也就随之改变了,但是p在coll中的位置却没变,这个时候我们调用contains方法时使用p的新的hashcode值来索引bucket时发现那个bucket位置没有元素,输出false。
通过上面的例子我们应该认识到,我们不应该使hashcode,equals方法依赖于可变变量。当这样的变量被加入集合时要注意不要修改它。但是如果我确实需要根据可变变量比较对象怎么办,那可以另外写一个方法,而不是equals。
错误四:equals方法不是一个等价关系
等价关系的定义具体见离散数学教材,简单就是自反的,对称的,传递的。
java规定equals方法的定义必须是一个等价关系,要求如下:
- It is reflexive: for any non-null value
x
, the expressionx.equals(x)
should returntrue
. - It is symmetric: for any non-null values
x
andy
,x.equals(y)
should returntrue
if and only ify.equals(x)
returnstrue
. - It is transitive: for any non-null values
x
,y
, andz
, ifx.equals(y)
returnstrue
andy.equals(z)
returnstrue
, thenx.equals(z)
should returntrue
. - It is consistent: for any non-null values
x
andy
, multiple invocations ofx.equals(y)
should consistently returntrue
or consistently returnfalse
, provided no information used in equals comparisons on the objects is modified. - For any non-null value
x
,x.equals(null)
should returnfalse
.
如下例:
public enum Color {
RED, ORANGE, YELLOW, GREEN, BLUE, INDIGO, VIOLET;
}
public class ColoredPoint extends Point { // Problem: equals not symmetric
private final Color color;
public ColoredPoint(int x, int y, Color color) {
super(x, y);
this.color = color;
}
@Override public boolean equals(Object other) {
boolean result = false;
if (other instanceof ColoredPoint) {
ColoredPoint that = (ColoredPoint) other;
result = (this.color.equals(that.color) && super.equals(that));
}
return result;
}
}
测试代码:
Point p = new Point(1, 2);
ColoredPoint cp = new ColoredPoint(1, 2, Color.RED);
System.out.println(p.equals(cp)); // prints true
System.out.println(cp.equals(p)); // prints false
例子中,p等于cp,但是cp不等p,这明显违反了等价关系中的对称性。原因也很清楚,那如何修改才能保持对称性呢,下面是另外一种错误的实现,解决了对称性却引入了传递性的问题
public class ColoredPoint extends Point { // Problem: equals not transitive
private final Color color;
public ColoredPoint(int x, int y, Color color) {
super(x, y);
this.color = color;
}
@Override public boolean equals(Object other) {
boolean result = false;
if (other instanceof ColoredPoint) {
ColoredPoint that = (ColoredPoint) other;
result = (this.color.equals(that.color) && super.equals(that));
}
else if (other instanceof Point) {
Point that = (Point) other;
result = that.equals(this);
}
return result;
}
}
为了测试这个版本违反了传递性,我们给出下面的测试:
ColoredPoint redP = new ColoredPoint(1, 2, Color.RED);
ColoredPoint blueP = new ColoredPoint(1, 2, Color.BLUE);
System.out.println(redP.equals(p)); // prints true
System.out.println(p.equals(blueP)); // prints true
System.out.println(redP.equals(blueP)); // prints false
例子的结果可以很明显的看出其中的问题。
下面给出一个技术上可行的方案
// A technically valid, but unsatisfying, equals method
public class Point {
private final int x;
private final int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() {
return x;
}
public int getY() {
return y;
}
@Override public boolean equals(Object other) {
boolean result = false;
if (other instanceof Point) {
Point that = (Point) other;
result = (this.getX() == that.getX() && this.getY() == that.getY()
&& this.getClass().equals(that.getClass()));
}
return result;
}
@Override public int hashCode() {
return (41 * (41 + getX()) + getY());
}
}
public class ColoredPoint extends Point { // No longer violates symmetry requirement
private final Color color;
public ColoredPoint(int x, int y, Color color) {
super(x, y);
this.color = color;
}
@Override public boolean equals(Object other) {
boolean result = false;
if (other instanceof ColoredPoint) {
ColoredPoint that = (ColoredPoint) other;
result = (this.color.equals(that.color) && super.equals(that));
}
return result;
}
}
当然这个版本太过严格了,Point和Coloredpoint类型的对象不可能相等了。
上面就是常见的错误类型,其实看到这篇文章我才知道写一个equals方法还是很困难的。
我自己写得equals方法如下:
public boolean equals(Object other)
{
if( other != null && other.getClass() == this.getClass())
{
Point p = (Point) other;
if( p.getX() == this.getX() && p.getY() == this.getY())
{
return true;
}
}
return false;
}
相对比较清楚,不知道有没有问题,如果大家觉得有问题可留言