天天看点

Effective Java - 对于所有对象都通用的方法 - 覆盖 equals 时请遵守通用约定

读书笔记 仅供参考

不覆盖 equals 方法

许多覆盖方式会导致错误,并且后果十分严重,最容易避免错误的方法就是不覆盖 equals 方法。每个类的实例都只与自身相等。

  • 类的每个实例本质上都是唯一的
  • 不关心类是否提供了“逻辑相等”的功能
  • 超类已经覆盖了 equals,从超类继承过来的行为对子类也是合适的(例如 List 从 AbstractList 继承 equals)
  • 如果类是私有的或包级私有,可以覆盖equals 方法,确保它的 equals 方法不会被意外调用
@Override
public boolean equals(Object o){
    throw new AssertionError();
}
           

何时覆盖 equals

如果类具有自己特有的“逻辑相等”概念,而且超类还没有覆盖 equals 。

这种类属于值类,例如 Integer 或 Data,只是想知道他们在逻辑上相等,并不像知道他们是否指向同一个对象。(如果是实例受控的值类,可以不覆盖 equals,因为每个值至多存在一个对象)

覆盖 equals 的通用规定

如果违反了这些规定,程序会表现不正常,甚至崩溃。

自反性

对于任何非 null 的引用值 x,x.equals(x) 必须返回 true。

对称性

对于任何非 null 的引用值 x 和 y,并且仅当 y.equals(x) 返回 true 时,x.equals(y) 必须返回 true。

错误的例子

//实现不区分大小写的字符串
public final class CaseInsensitiveString {
    private final String s;
    public CaseInsensitiveString(String s) {
        if(s == null) {
            throw new NullPointerException();
        }
        this.s = 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;
    }
}
           
CaseInsensitiveString cis = new CaseInsensitiveString("Polish");
String s = "polish";
cis.equals(s);//返回 true
//String 并不知道如何比较
s.equals(cis);//返回 false
           

解决方法:只比较 CaseInsensitiveString

传递性

对于任何非 null 的引用值 x,y 和 z,如果 x.equals(y) 返回 true,并且 y.equals(z) 返回 true,x.equals(z) 必须返回 true。

在考虑子类的情况下,这条约定很容易违背。

超类

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;
    }
}
           

子类

public class ColorPoint extends Point {
    private final Color color;
    public ColorPoint(int x, int y, Color color) {
        super(x, y);
        this.color = color;
    }
    //如果不提供 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(, );
ColorPoint cp =new ColorPoint(, , Color.RED);
p.equals(cp);//返回 true
cp.equals(p);//返回 false
           

改善版本

@Override
    public boolean equals(Object o) {
        if(!(o instanceof ColorPoint))
            return false;
        //是一个普通点,就忽略掉颜色信息
        if(!(o instanceof ColorPoint))
            return o.equals(this);
        //是一个彩色点,就全比较
        return super.equals(o) && ((ColorPoint) o).color == color;
    }
           

这种方法提供了对称性,缺失了传递性

ColorPoint p1 = new ColorPoint(, , Color.RED);
Point p2 = new Point(, );
ColorPoint p3 = new ColorPoint(, , Color.BLUE);
p1.equals(p2);//返回 true
p2.equals(p3);//返回 true
p1.equals(p3);//返回 false
           

我们无法在扩展可实例化的类的同时,既增加新的值组件,同时又保留 equals 约定

听说:在 equals 方法中用 getClass 代替 instanceof ,可以满足上面的要求

//Point 类
    @Override
    public boolean equals(Object o) {
        if(o == null || o.getClass() != getClass()) {
            return false;
        }
        Point p = (Point) o;
        return p.x == x && p.y ==y;
    }
           

但是这样违反了 里氏替换原则:一个类型的任何重要属性也将适用于它的子类型。当遇到类似 HashSet 的集合时,就无法将超类和子类都放进去了。

比较好的方法

采用复合,在 ColorPoint 中加入 Point 属性。

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;
    }

    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);
    }
}
           

ps:抽象类的子类可以增加新的值组件,而不会违反 equals 约定。

一致性

对于任何非 null 的引用值 x 和 y,只要 equals 的比较操作在所用的信息没有被改变,多次调用 x.equals(y) 就会一直返回同一个结果

非空性

对于任何非 null 的引用值 x,x.equals(null) 必须返回 false

实现 equals 方法的诀窍

诀窍一

使用 == 操作符检查 “参数是否是这个对象的引用”

诀窍二

使用 instanceof 操作符 检查 “参数是否为正确的类型”

诀窍三

把参数转换成正确的类型。

在转化之前进行 instanceof ,所以肯定会成功

诀窍四

对于该类中的每个“关键域”,检查是否相匹配

float : Float.compare()

double:Double.compare()

如果某些引用域包含 null 合法:

(field == null? o.field == null : field.equals(o.field))

如果通常是相同的对象引用:

(field == o.field || (filed != null && field.equals(o.field)))

提高性能:

先比较最有可能不一致的域

诀窍五

当 equals 完成后,要问自己:是否对称,传递,一致?

告诫

  • 覆盖 equals 时总要覆盖 hashCode
  • 不要企图让 equals 方法过于智能
  • 不要将 equals 声明的 Object 对象转换为其他的类型