8. 【对于所有对象都通用的方法】如何正确覆写equals方法

本文是《Effective Java》读书笔记第8条,其中内容可能会结合实际应用情况或参考其他资料进行补充或调整。


equals方法是Object类的方法,所以可以被任何类覆盖。但是在覆盖equals方法时,请一定要遵守相关的通用约定,否则容易出现后果严重的错误。
默认情况下,如果不覆盖这个方法,类的每个实例都只与它自身相等。这意味着类的每个实例本质上都是唯一的,直白点说就是每个实例在内存中的地址是唯一的;比如两个Student的对象,所有的成员值均相等(都是张三、都是25岁,身高体重也都一样等等),逻辑上,可以认为是一个人,但是默认的equals方法并不认为是相等的。

什么时候应该覆盖Object.equals方法呢

当类具有自己特有的“逻辑相等”概念的时候,说白了就是即使两个对象并不对应内存中的同一个实例,但是仅比较二者的内容是否相同的时候,比如刚才提到的两个张三的Student对象,虽然是两个对象,但逻辑上认为是相等的。当然,还有一点就是超类没有覆盖equals方法或超类的覆盖方法无法满足当前类的需要。
这些类通常属于“值类”,比如IntegerDateString等。
这些类的实例可以被用作Map的键key,或者Set的元素,因此确保其equals方法结果的准确性尤为重要。

有一种“值类”不需要覆盖equals方法,即用实例确保“每个值至多存在一个对象”的类,枚举类型就属于这种类。对于这样的类来说,逻辑相同与对象等同是一回事。

通用约定

在覆盖equals方法的时候,你必须要遵守它的通用约定,也就是等价关系(对于任何非null的引用值x、y和z):
1. 自反性。x.equals(x)必须返回true。
2. 对称性。当且仅当x.equals(y)返回true时,y.equals(x)必须返回true。
3. 传递性。如果x.equals(y)返回true,y.equals(z)返回true,那么x.equals(z)也必须返回true。
4. 一致性。只要被比较对象中所用的信息没有别修改,那么多次比较均返回同样的结果。
5. x.equals(null)必须返回false。

这些并不难理解,比如对于一个Point的对象来说:

class Point {
    private int x;
    private 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;
    }
}

那么对于Point的两个对象p1和p2:

Point p1 = new Point(1, 2);
Point p2 = new Point(1, 2);

问题就很简单了,p1.equals(p2)p2.equals(p1)都是true。

那么问题来了

但是有时也会碰到比较别扭的情况,比如:

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

现在ColorPoint类的equals方法继承自Point类,也满足刚才提到的那些约定,但是从业务上,明显不是我们想要的嘛。进一步补上equals方法:

    @override
    public boolean equals(Object o) {
        if (!(o instanceof ColorPoint))
            return false;
        return super.equals(o) && ((ColorPoint)o).color == color;
    }

然后我们来比较如下对象:

Point p = new Point(1, 2);
ColorPoint cp = new Point(1, 2, Color.RED);

那么问题来了,p.equals(cp)返回true,但是cp.equals(p)返回false。可以在传递参数是Point的时候忽略color的比较:

    @override
    public boolean equals(Object o) {
        if (!(o instanceof Point))
            return false;
        if (!(o instanceof ColorPoint))
            return o.equals(this);
        return super.equals(o) && ((ColorPoint)o).color == color;
    }

这样就可以保证对称性了,但是有牺牲了传递性:

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

这时候,p1.equals(p2)p2.equals(p3)返回true,但是p1.equals(p3)却返回false。

那么怎么解决呢?其实这是面向对象语言中关于等价关系的一个基本问题。我们无法再扩展可实例化的类的同时,既增加新的值组件,同时又保留equals约定,除非愿意放弃面向对象的特性所带来的优势。
当然,可以用getClass方法获取到class信息,然后比较是否是同一个类的实例,若是再比较,否则直接false,以Point类为例:

    @override
    public boolean equals(Object o) {
        if (o.getClass() != this.getClass())
            return false;
        Point p = (Point)o;
        return p.x == x && p.y == y;
    }

这样一定程度上解决了问题,但是却存在着严重的“后遗症”,那就是当用于MapSet集合的时候,Point的实例和ColorPoint的实例会被作为完全不同的对象对待,比如判断一个点是否位于半径为10的圆内时,ColorPoint(1, 2, Color.RED)并不会返回“是”。

寻找权宜之计

这时“复合优先于继承”的原则就显得颇为有用了。我们不再让ColorPoint继承自Point,而是在ColorPoint中增加私有的Point域,以及一个公有的视图方法,此方法返回一个与该有色点对应的普通Point对象。

class ColorPoint {
    private Point point;
    private Color color;

    public ColorPoint(int x, int y, Color color) {
        if (color == null)
            threw new NullPointException();
        this.point = new Point(x, y);
        this.color = color;
    }
    // 视图方法
    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);
    }
}

覆盖equals方法的正确姿势

  1. 使用==检查是否是同一个对象,如果比较操作比较费时,这种尝试是值得的;
  2. 使用instanceof检查是否为正确的类型;
  3. 把参数换成正确的类型;
  4. 对于该类中的每个关键域,检查参数中的域是否与该对象中对应的域匹配。具体来说:
    • 对于基本数据类型,如果是float和double,那么可以使用Float.compare和Double.compare方法,因为存在着Float.NaN,-0.0f等常量,其他用==比较;
    • 对于对象引用域,可以递归地调用equals方法;
    • 对于数组域,以上内容要应用到每个元素上,1.5之后的Arrays.equals方法就可以达到这一效果;
    • 应优先比较最有可能不同的域,或者开销最低的域
    • 有些对象的引用域包含null是合法的,因此为了避免出现异常,应该用以下的习惯用法来比较:
(field == null ? o.field == null : field.equals(o.field))
// 如果field和o.field通常是相同的对象引用,那么下边的方法快一些:
(field == o.field || (field != null && field.euqals(o.field)))
  1. 覆盖equals方法的时候同时也要覆盖hashCode方法;
  2. 一定注意equals方法声明中的参数为Object对象,如果换成其他的,那么就不是对Object类的equals方法的覆写,而是重载了,所以要加上@override注解,这样能够让编译器帮你检查出问题来。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值