如何写一个正确的equals方法

今天又见到一个覆写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 expression x.equals(x) should return true.
  • It is symmetric: for any non-null values x and yx.equals(y) should return true if and only if y.equals(x) returns true.
  • It is transitive: for any non-null values xy, and z, if x.equals(y) returns true and y.equals(z) returns true, then x.equals(z) should return true.
  • It is consistent: for any non-null values x and y, multiple invocations of x.equals(y) should consistently return true or consistently return false, provided no information used in equals comparisons on the objects is modified.
  • For any non-null value xx.equals(null) should return false.
这种错误主要集中于有继承的时候。

如下例:

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;
	}

相对比较清楚,不知道有没有问题,如果大家觉得有问题可留言













  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值