覆盖equals方法时要遵守的通用合约

覆盖equals方法并不像看上去那样简单,所以除非必要,还是不要覆盖为好。以下情况下,equals方法是不需要被覆盖的:

1.一个类的所有对象本质上说都是唯一的。比如Thread类,它的对象全都用来执行指令,因而他们在对象层面上的数据并不重要。这种情况下Object本身的equals实现已经足够。

2.对于某些类,我们并不在意对其对象间的"逻辑相等性"。比如java.util.Random。

3.父类已经定义了可正确应用于子类的equals。

4.私有类,并且可以确定其equals方法永远不会被执行。

除以上情况外,equals方法都需要被覆盖,覆盖后的equals方法,必须遵守以下合约:

1.自反性:任意对象与自身比较时equals必须返回true。这是很简单的一条规则,很难出现被违背的情况。

2.对称:如果a.equals(b)返回true,那么b.equals(a)也必须返回true。

这条看上去也同样简单,但实际上却很容易被破坏,比如下面这段代码:

// Broken - violates symmetry!
public final class CaseInsensitiveString {
	private final String s;
	public CaseInsensitiveString(String s) {
		if (s == null)
		throw new NullPointerException();
		this.s = s;
	}
	// Broken - violates symmetry!
	@Override public boolean equals(Object o) {
		if (o instanceof CaseInsensitiveString)
			return s.equalsIgnoreCase(
						((CaseInsensitiveString) o).s);
		if (o instanceof String) // One-way interoperability!
			return s.equalsIgnoreCase((String) o);
		return false;
	}
	... // Remainder omitted
}

这段代码的问题出在试图让覆盖的equals和String原生的equals方法互通,结果只能完成一半:

CaseInsensitiveString cis = new CaseInsensitiveString("Polish");
String s = "polish";

cis.equals(s)将返回true,而s.equals(cis)返回false。

需要注意的是如果对称性不被满足,程序的行为是不可预测的,而这样的bug在真实系统中将很难被发现。

上例的解决方法是,去掉与String互通的尝试,在与普通String对象比较时直接返回false:

@Override public boolean equals(Object o) {
	return o instanceof CaseInsensitiveString &&
							((CaseInsensitiveString) o).s.equalsIgnoreCase(s);
}

3.传递:如果a.equals(b)和b.equals(c)都返回true,那么a.equals(c)也必须返回true.

这是非常容易被破坏的一条,最常见的情况是子类继承父类时,增加了一些数据,并且影响了equals方法的行为:

public class Point {
	private final int x;
	private final int y;
	public Point(int x, int y) {
		this.x = x;
		this.y = y;
	}
	@Override public boolean equals(Object o) {
		if (!(o instanceof Point))
			return false;
		Point p = (Point)o;
		return p.x == x && p.y == y;
	}
	... // Remainder omitted
}

子类给Point加上了颜色属性:

public class ColorPoint extends Point {
	private final Color color;
	public ColorPoint(int x, int y, Color color) {
		super(x, y);
		this.color = color;
	}
	... // Remainder omitted
}

如果不在子类中对equals方法做些处理,沿用父类中的equals方法,新添加的color属性在比较操作中将被忽略。这虽然不违反任何一条合约,但这种行为不符合逻辑。如果简单覆盖equals方法,加上颜色属性,则类似于上一段String的例子,对称性会被破坏:

// Broken - violates symmetry!
@Override public boolean equals(Object o) {
	if (!(o instanceof ColorPoint))
		return false;
	return super.equals(o) && ((ColorPoint) o).color == color;
}

再修改一次,有普通Point参与比较时,忽略颜色属性,这时就是传递性被破坏了:

// Broken - violates transitivity!
@Override public boolean equals(Object o) {
	if (!(o instanceof Point))
		return false;
	// If o is a normal Point, do a color-blind comparison
	if (!(o instanceof ColorPoint))
		return o.equals(this);
	// o is a ColorPoint; do a full comparison
	return super.equals(o) && ((ColorPoint)o).color == color;
}

为证明这一点,定义三个Point对象:

ColorPoint p1 = new ColorPoint(1, 2, Color.RED);
Point p2 = new Point(1, 2);
ColorPoint p3 = new ColorPoint(1, 2, Color.BLUE);

可以看出p1.equals(p2)和p2.equals(p3)都返回true,但p1.equals(p3)却返回false。事实上,如果只应用继承架构,此例中的问题是无解的,因为它源于OO思想本身。继承一个非抽象类,并为其添加一个"值"属性,equals合约将无法被遵守。这种情况正确的做法,是使用组合(composition)而非继承,来实现这种属性的添加:

// Adds a value component without violating the equals contract
public class ColorPoint {
	private final Point point;
	private final Color color;
	public ColorPoint(int x, int y, Color color) {
		if (color == null)
			throw new NullPointerException();
		point = new Point(x, y);
		this.color = color;
	}
	/**
	* Returns the point-view of this color point.
	*/
	public Point asPoint() {
		return point;
	}
	@Override public boolean equals(Object o) {
		if (!(o instanceof ColorPoint))
			return false;
		ColorPoint cp = (ColorPoint) o;
		return cp.point.equals(point) && cp.color.equals(color);
	}
	... // Remainder omitted
}

4.一致:给定两个对象,如果两者本身的属性都没被修改,那么比较的结果不能随时间发生变化。为满足这一点,只需要注意一个原则,equals方法的行为不能依赖于可变的资源或数据。

5.非空:很简单,非null对象与null比较时,equals必须返回false。

提交评论


安全码
刷新