覆盖equals时必须遵守的约定

该不该覆盖equals?

覆盖equals是比较困难的,最容易避免的方式是不覆盖,在这种情况下类的每一个实例都与他自身相等。
满足下面的条件我们就可以不覆盖:

  • 类的实体本质是唯一的 : 对于实体代表的不是值而是类。
  • 根本不关心equals方法的相关类:
  • 超类已经覆盖了,而且超类覆盖的equals方法对子类也同样适用:如容器
  • 类是私有的或者包级私有的保证他的equals方法永远不会被调用

那什么时候我们需要覆盖呢,如果类自身有特有逻辑相等的概念,而且超类并没有覆盖实现期望的行为,我们就需要覆盖equals方法,这种类同时就是所谓的"值类" 如:Interger或者Date呀。 但是也有例外:实例受控(每个值至多指向一个对象)或者枚举类型的类。

equals覆盖的约定

  • 自反性 : 自身 equals ture 自身,如果违反这一点,如我们添加实例到Set中的话,contians方法会判断不了是不是有实例在容器中。
  • 对称性:a.equals(b)==tureb.equals(a)==ture ,如一个不区分大小的String和区分大小写的String就违反了对称性,如果添加进容器,错误更是不明。
  • 传递性 : (a -> b == ture & & b -> c == ture) => a->c == ture,比较难解决的点如果向一个无颜色的点 和有颜色的点比较(p33),会暴露java的基本问题:我们无法在可实例化的同时,即增加新组件,又同时保留equals的约定,但是有两个权衡之计 :
  1. 复合大于继承: 我们让子类不继承,而是持有私有父类的引用属性,子类也不在继承与父类,自然在equals的时候就不用考虑相关问题了
  2. 让可实例化变成不可实例化,如变为抽象类
  • 一致性 : 只要类的所用的信息不修改 ,不论调用equals多少次,返回结果都是一样的。我们应该先确定对象是否可变,如果不可变,那么相等一辈子都相等。而且equals不要依赖于不可靠资源。
  • 非空性:对于非null ,调用 a.equals(null),返回必定为空,但是我们一般都不用太关心,因为首先我们都会调用instanceof

我们必须十分谨慎的修改,因为我们调用equals方法,都依赖于它们对象,所以如果发生相关的错误是很难发现。

写高质量eqauls的诀窍

  • 使用 == 操作符检查参数是不是这个对象的引用

  • 使用instanceof检查参数是不是正确的类型

  • 把参数转为正确的类型,但在这之前得用 instanceof检查一遍

  • 对于类的每一个关键的域,都要检查参数中的域是不是相同:如果类型是接口就调用接口的方法,如果是类就访问可访问类(不一定)

    1. 基本类型用 ‘==’
    2. 对于应用于用 equals方法链
    3. 对于float或者double域 用 .compare
    4. 某些引用域为null是合法的为了避免空指针异常需注意
  • 为了保证性能,最先比较的是最可能不一致的域,或者是开销最低的域

  • 覆盖equals总要覆盖hashcode()

  • 不要让equals过于智能,这样会增加工作量还得不偿失

  • 千万不要将equals声明的Object的对象替换成相关类型,这样他就成了重载,而不是覆盖,这会严重的增加复杂度。

  • 写完反问自己是不是对称,是不是一致,是不是传递

推荐阅读更多精彩内容