Effective Java笔记 :覆盖 equals 时请遵守通用约定
在《Effective Java》中,Item 10:覆盖 equals 时请遵守通用约定 是一个重要的指导原则。equals 方法是 Java 对象中最核心的方法之一,正确地覆盖它不仅可以让程序运行更可靠,还能避免由于设计错误导致的潜在问题,例如违反对称性或一致性等。
下面我们会详细讲解覆盖 equals 方法的通用约定、注意事项、以及一个示例实现。
1. 为什么需要覆盖 equals 方法?
默认行为(Object 的 equals)
equals 方法在 java.lang.Object 中默认实现如下:
public boolean equals(Object obj) {
return this == obj; // 默认比较的是对象的引用
}
这一默认实现检查两个对象是否是同一个对象(内存地址是否相同)。这种行为对于大部分类(例如用作某种特定唯一身份对象)是足够的,但对于某些类,比如值类型类(value classes),通常需要比较它们的内容是否相同,而不是引用相等。
什么时候需要覆盖 equals?
当一个类需要通过值相等来判断两个对象是否相同时(而不是引用相等),就应该覆盖 equals。
比如:
String类已经覆盖了equals,比较内容而不是引用。Integer类也覆盖了equals。
2. 通用约定(契约)
覆盖 equals 时,必须遵守以下约定,这些约定定义了 equals 的行为:
2.1 自反性(Reflexive)
- 对任意非空引用值
x,x.equals(x)必须返回true。 - 一个对象必须与自身相等。
2.2 对称性(Symmetric)
- 对任意引用值
x和y,如果x.equals(y)返回true,则y.equals(x)也必须返回true。 - 比较结果必须互相一致。
2.3 传递性(Transitive)
- 对任意引用值
x、y和z,如果x.equals(y)为true,且y.equals(z)为true,那么x.equals(z)必须为true。 - 这保证了
equals的一致性。
2.4 一致性(Consistent)
- 对任意引用值
x和y,只要对象未改动,x.equals(y)的比较结果就必须一致(无论调用几次都返回相同的值)。
2.5 对 null 的比较
- 对任意非空引用值
x,x.equals(null)必须返回false。
3. 覆盖 equals 方法的注意事项
3.1 使用 instanceof 检查类型
- 确保传入的对象类型是正确的,避免出现
ClassCastException。
3.2 比较每个字段的值
- 对各个关键字段进行比较来判断对象是否相等。
3.3 返回 boolean 类型
- 始终返回布尔值
true或false,而不是抛异常或者返回其他值。
3.4 避免对浮点数直接比较
- 对于
float和double,在比较其值时要考虑精度问题; - 可以使用
Double.compare或Float.compare方法。
4. 示例:一个正确覆盖 equals 的实现
假设我们有一个表示二维点的类 Point,需要覆盖 equals 方法。
public class Point {
private final int x;
private final int y;
// 构造器
public Point(int x, int y) {
this.x = x;
this.y = y;
}
// 覆盖 equals 方法
@Override
public boolean equals(Object obj) {
// 1. instanceof 检查类型
if (!(obj instanceof Point)) {
return false; // 非同类型返回 false
}
// 2. 进行类型转换
Point other = (Point) obj;
// 3. 比较关键字段
return this.x == other.x && this.y == other.y;
}
// 最好还要覆盖 hashCode 方法(后续会讨论)
@Override
public int hashCode() {
return 31 * x + y; // 简单的哈希实现,确保一致性
}
}
测试类:验证遵守约定
public class Main {
public static void main(String[] args) {
Point p1 = new Point(1, 2);
Point p2 = new Point(1, 2);
Point p3 = new Point(3, 4);
// 自反性
System.out.println("自反性: " + p1.equals(p1)); // true
// 对称性
System.out.println("对称性: " + p1.equals(p2)); // true
System.out.println("对称性: " + p2.equals(p1)); // true
// 传递性
Point p4 = new Point(1, 2);
System.out.println("传递性: " + p1.equals(p2)); // true
System.out.println("传递性: " + p2.equals(p4)); // true
System.out.println("传递性: " + p1.equals(p4)); // true
// 一致性
System.out.println("一致性: " + p1.equals(p3)); // false
System.out.println("一致性: " + p1.equals(p3)); // false
// 对 null 的检查
System.out.println("null 检查: " + p1.equals(null)); // false
}
}
输出:
自反性: true
对称性: true
对称性: true
传递性: true
传递性: true
传递性: true
一致性: false
一致性: false
null 检查: false
5. 需要同时覆盖 hashCode
约定:如果覆盖了 equals,必须同时覆盖 hashCode,并确保逻辑一致性。
equals 和 hashCode 的一致性要求:
- 如果两个对象的
equals方法返回true,它们的hashCode方法也必须返回相同的值。 - 如果两个对象的
equals方法返回false,它们的hashCode方法不强制要求不同。
为什么需要一致性?
- 在集合中使用(例如
HashMap、HashSet)时,hashCode会影响哈希表的存储和查找行为。如果equals和hashCode不一致,集合可能出现较大的性能问题,甚至逻辑错误。 equals和hashCode共同确保对象的一致性。
6. equals 方法的设计建议
6.1 不要覆盖 equals(默认实现足够时)
如果类的实例不需要比较内容(即始终只需要比较引用),无需覆盖 equals,默认实现已经足够。
6.2 避免滥用 instanceof
如果对象应该实现严格的相等检查(包括类的子类比较),可以使用 getClass() 而不是 instanceof。
- getClass() 是 java.lang.Object 中的方法,用于返回运行时对象的具体类(即实际类型)的 Class 实例。
- 用 getClass() 检查类型时,它只会返回对象的声明类,不会考虑继承链。
- 如果两个对象属于不同的具体类(即使其中一个是另一个的子类),getClass() 判断它们为不同类型。
- instanceof 是一种用于检查一个对象是某个类或其子类的实例的运算符。使用 instanceof 时,会考虑继承关系,可以用来判断类的子类是否属于同一类型。
这种行为更松散,因为它允许子类在类型检查时被认作父类。
示例:
@Override
public boolean equals(Object obj) {
if (obj == null || obj.getClass() != getClass()) {
return false;
}
// 自定义逻辑
}
6.3 谨慎处理浮点数比较
浮点数可能涉及精度问题,建议使用 Double.compare 或 Float.compare 方法。
7.子类新增比较字段的问题
如果你扩展了一个具体类(一个不是 abstract 的类),并且想在子类中引入新字段(值组件),则很难在保留这些约定的同时正确实现 equals。原因在于父类与子类之间的继承关系导致设计出了矛盾的逻辑。
具体举例:颜色点(Point 和 ColorPoint)问题
假设我们有一个简单的 Point 类,表示二维坐标系中的点。
父类 Point:
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 (this == o) {
return true;
}
if (!(o instanceof Point)) {
return false;
}
Point point = (Point) o;
return x == point.x && y == point.y;
}
}
Point 的 equals 方法比较其 x 和 y 坐标,判断两个点是否相等。
子类 ColorPoint:
现在我们扩展 Point 类,增加颜色字段:color。
public class ColorPoint extends Point {
private final String color; // 新的值组件:颜色
public ColorPoint(int x, int y, String color) {
super(x, y);
this.color = color;
}
@Override
public boolean equals(Object o) {
if (!(o instanceof ColorPoint)) {
return false;
}
ColorPoint that = (ColorPoint) o;
return super.equals(o) && this.color.equals(that.color);
}
}
出现的问题:打破 equals 的对称性
我们来看这个例子会发生什么:
public static void main(String[] args) {
Point p = new Point(1, 2);
ColorPoint cp = new ColorPoint(1, 2, "red");
System.out.println(p.equals(cp)); // true (从 Point 的 perspective 只检查 x 和 y)
System.out.println(cp.equals(p)); // false(从 ColorPoint 的 perspective 检查 x, y, color)
}
违反了对称性!
p.equals(cp)返回true,因为Point的equals实现只比较了x和y,并不关心是否是ColorPoint的实例。- 但
cp.equals(p)返回false,因为ColorPoint的equals要求不仅x和y相同,还需要color相同(p没有颜色数据)。
进一步问题:违反传递性
现在,我们再引入另一个颜色点:
ColorPoint cp_red = new ColorPoint(1, 2, "red");
ColorPoint cp_blue = new ColorPoint(1, 2, "blue");
Point p = new Point(1, 2);
如果按默认实现,这样的行为会违反传递性:
System.out.println(cp_red.equals(p)); // true (ColorPoint 和 Point 只比较 x 和 y)
System.out.println(p.equals(cp_blue)); // true (Point 和 ColorPoint 也只比较 x 和 y)
System.out.println(cp_red.equals(cp_blue)); // false (ColorPoint 额外比较 color)
违反了传递性:
cp_red.equals(p)返回truep.equals(cp_blue)返回true- 但
cp_red.equals(cp_blue)返回false
** 为什么无法解决这个问题?**
这个问题的根本原因是继承 Point 和 ColorPoint 之间对 equals 的要求不可兼容:
Point的equals定义只比较x和y。ColorPoint的equals不仅要比较x和y,还要比较color。
在继承关系中,子类通常需要扩展父类,而不是减少功能。然而,equals 的传递性要求比较的逻辑必须是固定的,不能根据子类的扩展而改变。
总之,这种扩展导致逻辑矛盾:
- 如果
Point.equals忽略color,它会打破对称性。 - 如果
ColorPoint.equals强制要求所有对象都包含color,就和父类逻辑不兼容。
如何应对这个问题?
方案 1:限制继承(违反里氏替换原则)
最简单的办法是,不允许子类为 Point 添加新的值组件。换言之:
- 如果设计了一个支持
equals方法的类(比如Point),让这个类不允许被继承。
可以通过将 Point 声明为 final 来避免继承。
public final 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 (this == o) {
return true;
}
if (!(o instanceof Point)) {
return false;
}
Point point = (Point) o;
return x == point.x && y == point.y;
}
}
如果 Point 是不可扩展的,类的等价性逻辑也会变得清晰:它永远只需要比较 x 和 y。
方案 2:使用组合而不是继承
另一种解决方法是,避免直接继承,而通过组合来表达扩展的需求。
public class ColorPoint {
private final Point point; // 组合关系
private final String color;
public ColorPoint(int x, int y, String color) {
this.point = new Point(x, y);
this.color = color;
}
@Override
public boolean equals(Object o) {
if (!(o instanceof ColorPoint)) {
return false;
}
ColorPoint cp = (ColorPoint) o;
return point.equals(cp.point) && color.equals(cp.color);
}
@Override
public int hashCode() {
return point.hashCode() * 31 + color.hashCode();
}
}
通过组合,ColorPoint 的等价性检查不再打破 Point 的逻辑,因为 Point 的等价逻辑与子类(ColorPoint)之间没有直接耦合关系。
方案 3:用不同类型的方式比较
如果确实需要支持父类和子类之间带值组件的比较,可以提供额外的比较逻辑(如提供一个自定义比较器),而不是直接扩展 equals 方法。
8. equals 的测试工具
- JUnit:
使用 JUnit 可以对equals方法进行系统性测试。 - Apache Commons EqualsBuilder:
Apache Commons Lang 提供了一个EqualsBuilder工具类,可以快速实现equals方法。
381

被折叠的 条评论
为什么被折叠?



