用了这么久的equals,你知道还要遵守约定么(下)

简介: 重写equals 方法看起来很简单,但是还会有多种方式导致出错,后果可能是严重的。最简单,最容易避免出错的方式是 避免重写equals方法 ,采用这种方式的每个类只需要和自己对比即可,这样永远不会出错。如果满足了以下任何一个约定,也能产生正确的结果:
@Override
public boolean equals(Object o) {
  if(!(o instanceof ColorPoint)){
    return false;
  }
  return super.equals(o) && ((ColorPoint)o).color == color;
}

这种方法的问题在于,在比较普通点和有色点时,以及相反的情形可能会得到不同的结果。前一种比较忽略了颜色信息,而后一种比较返回 false,因为参数类型不正确。为了直观说明问题,我们创建一个普通点和一个有色点来进行测试

public static void main(String[] args) {
  Point p = new Point(1,2);
  ColorPoint cp = new ColorPoint(1,2,Color.RED);
  System.out.println(p.equals(cp));
  System.out.println(cp.equals(p));
}

p.equals(cp) 调用的是 Point 中的 equals 方法,而此方法中没有关于颜色的比较,之比较了 x 和 y


cp.equals(p) 调用的是 ColorPoint 中的 equals 方法,而此方法中有关于颜色的比较,而 p 中没有颜色信息


你可以这样做来修正这个问题

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 cp = new ColorPoint(1,2,Color.RED);
Point p = new Point(1,2);
ColorPoint cp2 = new ColorPoint(1,2,Color.BLUE);

此外,还可能会导致无限递归问题,比如 Point 有两个字类,分别是 ColorPoint 和 SmellPoint,它们各自有自己的 equals 方法,那么对 myColorPoint.equals(mySmellPoint)的调用将会抛出 StackOverflowError 异常。


你可能听过使用 getClass 方法替代 instanceof 测试,可以扩展可实例化的类和增加新的组件,同时保留 equals 约定,例如

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

里氏替换原则认为,一个类型的任何属性也将适用于它的字类型


一个不错的改良措施是使用 组合优先于继承 的原则,我们不再让 ColorPoint 扩展 Point,而是让 ColorPoint 持有一个 Point 的私有域,以及一个公有视图方法,例如

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

在 Java 平台类库中,有一些类扩展了可实例化的类,并且添加了新的组件值。


例如:java.sql.Timestamp 对 java.util.Date 进行了扩展,并添加了 nanoseconds 域。Timestamp 类与 Date 类进行 equals 比较时会发生不可预期的行为,虽然工程师在 Timestamp 告诫不要和 Date 类一起使用,但是这种行为依旧不值得效仿。


一致性


equals 约定的第四个要求是,如果两个对象相等,它们就必须保证始终相等,除非它们中有一个对象(或者两个都)被修改了。也就是说,可变对象在不同的时候可以与不同的对象相等。不可变对象不会这样,它们会保证始终相等。


无论类是否可变,都不要使 equals 方法依赖于不可靠的资源。例如,java.net.URL 的 equals 方法依赖于对 URL中主机IP 地址的比较。将一个主机名转变成 IP 地址可能需要访问网络,随着时间的推移,就不能确保会产生相同的结果,即有可能 IP 地址发生了改变。这样会导致 URL  equals 方法违反 equals 约定,在实践中有可能引发一些问题。URL equals 方法的行为是一个大错误并且不应被模仿。遗憾的是,因为兼容性的要求,这一行为元法被改变。为了避免发生这种问题,equals 方法应该对驻留在内存中的对象执行确定性的计算。


非空性


非空性的意思是所有的对象都不能为 null 。尽管很难想象什么情况下 o.equals(null) 会返回 true。但是意外抛出空指针异常的情形可不是很少见。通常不允许抛出 空指针异常,许多类的 equals 方法都通过对一个显示的 null 做判断来防止这种情况:

public boolean equals(Object o) {
  if(o == null){
    return false;
  }
}

这项测试是不必要的。为了测试其参数的等同性,equals 方法必须先把参数转换成适当的类型,以便可以调用它的访问方法,或者访问它的域。


如果漏掉了类型检查,有传递给 equals 方法错误的类型,那么 equals 方法将会抛出 ClassCastException,这就违反了 equals 约定。如果 instanceof 的第一个操作数为 null ,那么,不管第二个操作数是哪种类型,intanceof 操作符都指定应该返回 false 。因此,如果把 null 传给 equals 方法,类型检查就会返回 false ,所以不需要显式的 null 检查。


遵循如下约定,可以实现高质量的空判断:


  • 使用 == 操作符检查 参数是否为这个对象的引用 。如果是,返回 true 。
  • 使用 instanceof 操作符检查 参数是否为正确的引用类型。如果不是,则返回 false。
  • 对于该类中的每个域,检查参数中的域是否与该对象中对应的域相匹配。


编写完成后,你还需要问自己: 它是否是对称的、传递的、一致的?


下面是一些告诫:


  • 覆盖 equals 时总要覆盖 hashCode
  • 不要企图让 equals 方法过于智能
  • 不要将 equals 声明中的 Object 对象替换为其他的类型。
相关文章
|
7月前
|
Arthas SQL Java
不规范的枚举类代码引发的一场事故
作者参与了一个问题排查,最后得到的结论和枚举类的规范有关系,本文将过程总结在这里提供大家一起学习交流。
|
存储 设计模式 缓存
新来了个同事,代码命名规范是真优雅呀!代码如诗!!
新来了个同事,代码命名规范是真优雅呀!代码如诗!!
|
存储 缓存 监控
新来了个同事,代码命名规范是真优雅呀!代码如诗!! 上
新来了个同事,代码命名规范是真优雅呀!代码如诗!! 上
|
设计模式 XML 缓存
新来了个同事,代码命名规范是真优雅呀!代码如诗!! 下
新来了个同事,代码命名规范是真优雅呀!代码如诗!! 下
|
缓存 Java 应用服务中间件
不规范使用ThreadLocal导致的bug,说多了都是泪
ThreadLocal一般用于线程间的数据隔离,通过将数据缓存在ThreadLocal中,可以极大的提升性能。但是,如果错误的使用Threadlocal,可能会引起不可预期的bug,以及造成内存泄露。
250 0
不规范使用ThreadLocal导致的bug,说多了都是泪
|
设计模式 消息中间件 JavaScript
代码越写越乱?那是因为你没用责任链
代码越写越乱?那是因为你没用责任链
代码越写越乱?那是因为你没用责任链
|
JavaScript Java 索引
针对EL表达式的本质以及规范的透析
EL(Expression Language) 是为了使JSP写起来更加简单。表达式语言的灵感来自于 ECMAScript 和 XPath 表达式语言,它提供了在 JSP 中简化表达式的方法,让Jsp的代码更加简化。
针对EL表达式的本质以及规范的透析
|
存储 安全 Java
static关键字真能提高Bean的优先级吗?答:真能(下)
static关键字真能提高Bean的优先级吗?答:真能(下)
static关键字真能提高Bean的优先级吗?答:真能(下)
|
Java 中间件 程序员
static关键字真能提高Bean的优先级吗?答:真能(上)
static关键字真能提高Bean的优先级吗?答:真能(上)
static关键字真能提高Bean的优先级吗?答:真能(上)
|
Java Spring
static关键字真能提高Bean的优先级吗?答:真能(中)
static关键字真能提高Bean的优先级吗?答:真能(中)