Effective Java读书笔记七

来源:互联网 发布:windows rt 越狱 编辑:程序博客网 时间:2024/05/24 15:42

          

Item 8:覆写equals方法应该遵守的准则

最简单的准则就是不覆写equals方法,这样就能避免很多问题。以下的这些情况不需要覆写equals方法:

  • 类的每一个实例都是唯一的:比如Thread,我们关注的是它本身作为一个活动的实体,而不是一个值。
  • 我们不关注某个类是否是逻辑相等的:比如java.util.Random类覆写了equals方法来检查两个Random的实例是否会产生相同的随机数,但是这个对我们来说基本毫无用处。
  • 超类已经覆写了equals方法,并且该覆写对子类也适用:就像AbstractSet,AbstractList等类。
  • 类的可见性是private或者package-private,并且我们确信没有人会调用它的equals方法。

当一个类需要使用对象ID以外的东西作为逻辑相等并且超类也没有提供相应的功能时,尤其是value classes,我们就需要覆写equals方法。当然,覆写equals方法不但需要满足我们逻辑相等的要求,而且对象的实例还需要满足map的key或者set的行为。不过我们在Item1里提到的控制对象实例的类不需要覆写equals方法。

根据Java规范,equals需要满足一下特性:

  • 自反性(Reflexive):一个非null的对象x满足x.equals(x) == true;这个特性不大可能违反,所以不用详细讨论了。
  • 对称性(Symmetric):非null对象x和y满足x.equals(y) == y.equals(x);这个特性非常容易违反,所以一定要谨慎。
  • 传递性(Transitive):非null对象x、y和z,当x.equals(y)==true,y.equals(z)==true时,x.equals(z)==true;肯定会违反这个特性的情况是子类增加了某些字段影响了equals方法。比如我们有一个用来表示二维空间上点的类Point:

    假设我们继承这个类,增加一个color属性:

    现在问题就很麻烦了:新的子类ColorPoint怎么来覆写equals。如果不覆写的话是不会违反我们的这些特性的,但是直接使用Point的equals方法显然不适合子类。如果在ColorPoint中只比较ColorPoint类型,如:

     这种写法直接违反了对称性,假如我们创建了两个Point:
    那么p.equals(cp)是true,但是cp.equals(p)却为false。如果我们根据这个情况在ColorPoint的equals方法中加入对Point的判断,则又会违反传递性:
     
    此时,当我们有三个Point的时候:

    p1.equals(p2)为true,p2.equals(p3)也为true,但是p1.equals(p3)为false。
    所以说,对于可以被实例化的类,当它的子类加入新的value属性时,我们无法为它覆写equals方法。
    有人说可以用getClass方法代替instanceof,但是这么做实际上违反了LSP。因此,Joshua建议在这种情况下可以考虑对象的组合来代替类的继承,比如上面提到的ColorPoint,我们可以创建一个单独的类ColorPoint,它内部持有Point和Color对象,这样再覆写equals方法时就不会出现问题了。
    值得注意的是在Java的标准库中也有这样有问题设计的类,比如java.sql.Timestamp继承了java.util.Date类,增加了一个nanoseconds属性,Timestamp类中的equals方法就违反了对称性。
    另外,对抽象类的子类增加value属性不会违反equals的特性,因为抽象类不存在实例。
  • 一致性(Consistent):非null对象x和y,只要x和y不改变,多次调用x.equals(y)所得到的结果是一样的;这个特性需要在immutable对象上着重注意一下,但是不管这个对象是否是immutable的,我们的equals方法都不能依赖不可靠的资源来做判断。比如java.net.URL的equals方法就依赖了URL对应的IP地址来比较,这显然是不靠谱的。当我们的网络链接出现问题时,这就会破坏掉equals的特性。
  • 非null性(Non-nullity):对于任意非null对象x,x.equals(null)总是等于false;这个也很难违反。很多人喜欢在覆写equals方法时第一句判断参数是否为null,实际上我们的instanceof方法已经帮我们做了,所以这个判断可以不做。

千万不要违反上面的这些特性,否则程序就会出现难以调试的莫名错误。

最后,总结一下写出高质量equals方法的步骤:

  1. 如果参数引用的是对象自己,那么使用==操作符来比较,这样对性能也是帮助;
  2. 使用instanceof来判断equals传入的参数是否为正确的类型;
  3. 将参数转换为对应的类型;
  4. 比较对象本身和传入的参数,检查每个需要判断的字段:原始类型除了float和double,其它统统用==来比较;对于float则使用Float.compare而double则使用Double.compare;对于数组类型,应该把需要比较的元素依次拿出来做比较,如果需要全部元素比较的话,用Arrays.equals方法就好;至于引用类型,先判断是否为null,之后再判断是否相等。
    关于比较的性能问题,最好先比较容易改动的字段,不要去比较那些和逻辑比较无关的东西,比如用来同步的Lock字段。另外,由关键字段计算得出的东西也不需要比较。
  5. 当完成equals的覆写后,问自己三个问题:是否满足对称性?是否满足传递性?是否满足一致性?当然,不能只问,要多写测试。

另外附加一些注意事项:

  • 覆写equals方法的同时必须覆写hashCode
  • 别省事,要写就踏踏实实的写
  • 别把equals的参数类型改掉,记住原始equals的参数类型是Object,改掉就成重载了,覆写的时候方法前加上@Override。