Java高效编程(10):重写equals时必须遵循通用约定
解锁Python编程的无限可能:《奇妙的Python》带你漫游代码世界
重写 equals
方法看似简单,但极易出错,且后果严重。要避免问题,最简单的方法就是不重写 equals
,这样每个类的实例只与自身相等。如果以下情况适用,那么不重写 equals
是正确的选择:
- 类的每个实例都是独一无二的:例如
Thread
类,其代表的是活跃实体,而不是值,默认的equals
方法已经适用。 - 类不需要逻辑上的相等性测试:例如
Pattern
类可以重写equals
来检查两个正则表达式是否相同,但设计者认为不需要这个功能。 - 超类已经重写了
equals
并且该行为适用于子类:例如,大多数Set
实现继承自AbstractSet
的equals
,List
实现继承自AbstractList
,Map
实现继承自AbstractMap
。 - 类是私有或包私有的,且你确定
equals
不会被调用:可以通过重写equals
方法抛出AssertionError
来确保该方法不会被意外调用。
何时应该重写 equals
?
当类有逻辑上的相等性要求且超类没有重写 equals
时,就应该重写。通常这是值类的情况,例如 Integer
或 String
。程序员期望通过 equals
比较这些值类的逻辑等价性,而不是对象的引用是否相同。重写 equals
方法不仅是为了满足程序员的预期,也是为了让实例在作为键或集合元素时表现一致且可预测。
不需要重写 equals
的情况之一是使用实例控制(【条目1】)确保每个值最多只有一个对象。例如 Enum
类型,因为逻辑等价就是对象标识,Object
的 equals
方法已足够。
equals
合同
重写 equals
时,必须遵循 equals
的一般合同。合同规定 equals
实现等价关系,具体包括以下五个属性:
- 自反性:对于任何非空引用值
x
,x.equals(x)
必须返回true
。 - 对称性:对于任何非空引用值
x
和y
,x.equals(y)
必须返回与y.equals(x)
相同的结果。 - 传递性:如果
x.equals(y)
为真,且y.equals(z)
为真,那么x.equals(z)
必须为真。 - 一致性:只要
equals
比较中使用的信息没有被修改,x.equals(y)
的多次调用必须始终返回相同的结果。 - 非空性:对于任何非空引用值
x
,x.equals(null)
必须返回false
。
这些要求听起来复杂,但理解后不难遵守。如果违反合同,可能导致程序行为不稳定,甚至崩溃,且错误源头难以定位。
详细解释 equals
合同
自反性
自反性要求对象必须与自身相等。违反这一要求很少见。如果违反它,可能导致集合中的 contains
方法无法找到刚添加的对象。
对称性
对称性要求两个对象必须对其相等性意见一致。例如,以下类实现了一个忽略大小写的字符串:
// 错误示例 - 违反对称性
public final class CaseInsensitiveString {private final String s;public CaseInsensitiveString(String s) {this.s = Objects.requireNonNull(s);}@Override public boolean equals(Object o) {if (o instanceof CaseInsensitiveString)return s.equalsIgnoreCase(((CaseInsensitiveString) o).s);if (o instanceof String)return s.equalsIgnoreCase((String) o);return false;}
}
上面的 equals
方法试图与普通字符串互操作,但这导致了对称性问题:cis.equals(s)
返回 true
,而 s.equals(cis)
返回 false
。为解决此问题,可以去掉与 String
的互操作代码:
@Override public boolean equals(Object o) {return o instanceof CaseInsensitiveString &&((CaseInsensitiveString) o).s.equalsIgnoreCase(s);
}
传递性
传递性要求如果 x.equals(y)
为真且 y.equals(z)
为真,那么 x.equals(z)
也必须为真。考虑扩展类 Point
添加颜色属性的场景:
public class ColorPoint extends Point {private final Color color;public ColorPoint(int x, int y, Color color) {super(x, y);this.color = color;}@Override public boolean equals(Object o) {if (!(o instanceof ColorPoint))return false;return super.equals(o) && ((ColorPoint) o).color == color;}
}
此方法违反了对称性和传递性:p.equals(cp)
返回 true
,而 cp.equals(p)
返回 false
。为解决此问题,使用组合而不是继承:
public class ColorPoint {private final Point point;private final Color color;public ColorPoint(int x, int y, Color color) {point = new Point(x, y);this.color = Objects.requireNonNull(color);}@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);}
}
一致性
一致性要求对象在未被修改时,相等性必须一致。如果类是可变的,则要确保修改后 equals
的结果也保持一致。
非空性
对象必须与 null
不相等。即使传递 null
时 equals
返回 false
,也不应抛出 NullPointerException
。这可以通过 instanceof
检查来实现。
编写高质量的 equals
方法
- 使用
==
检查参数是否为当前对象。 - 使用
instanceof
检查参数类型。 - 将参数转换为正确类型。
- 比较所有“重要”字段,确保它们的值相等。
结论
不应轻易重写 equals
,除非有必要。如果需要重写,务必确保比较类的所有重要字段,并遵守 equals
合同的五个规定。对于复杂的值比较,应考虑使用组合而非继承。