×

消息

EU e-Privacy Directive

This website uses cookies to manage authentication, navigation, and other functions. By using our website, you agree that we can place these types of cookies on your device.

View e-Privacy Directive Documents

You have declined cookies. This decision can be reversed.

覆盖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。

提交评论


安全码
刷新