美文网首页Android开发程序员Android开发经验谈
高效Java第八条覆盖equals时请遵守通用约定

高效Java第八条覆盖equals时请遵守通用约定

作者: 每天学点编程 | 来源:发表于2017-07-23 08:58 被阅读0次

    尽管Object是一个具体类,但是设计它主要是为了扩展。它所有的非final方法(equalshashCodetoStringclonefinalize)都有明确的通用约定,这些方法被设计成要被覆盖的。任何一个类,在覆盖这些方法的时候,都有责任遵守这些通用约定;如果不能做到这一点,其他依赖于这些约定的类就无法结合该类一起正常工作。

    有许多覆盖equals方法的方式会导致错误,并且后果非常严重。最容易避免这类问题的办法就是不覆盖equals方法,在这种情况下,类的每个实例都只与它自身相等。

    不需要覆盖equals方法的情况

    类的每个实例本质上都是唯一的。对于代表活动实体而不是值的类来说确实如此,例如ThreadObject提供的equals实现对于这些类来说是正确的行为。
    不关心类是否提供“逻辑相等”的测试功能。java.util.Random可以覆盖equals,以检查两个Random实例是否产生相同的随机数序列,但是这样的功能是没有价值的。
    超类已经覆盖了equals,从超类继承而来的行为对于子类也是合适的。Set实现都从AbstractSet继承了equals的实现;List实现从AbstractList继承equals实现;Map实现从AbstractMap继承equals实现。

    类是私有的或包级私有的,可以确定它的equals方法永远不会被调用。必须覆盖equals方法,以防止它被意外调用:

    实例受控的值类不需要覆盖equals方法,因为实例受控的值类可以确保“每个值至多只存在一个对象”。例如枚举类型。实例受控的值类的实例逻辑相同与对象等同是一回事。

    需要覆盖Object.equals的情况

    值类——类具有自己特有的“逻辑相等”的概念(不同于对象等同),而且超类没有覆盖equals实现期望的行为,这时需要覆盖equals方法。
    值类:仅仅是一个表示值的类,例如IntegerDate
    使用equals比较值对象的引用,是比较它们在逻辑上是否相等,而不是确认它们是否指向同一个对象。

    覆盖equals方法的目的

    覆盖equals方法,要让该类的实例可以做Map的键,或是Set的元素,使映射或集合表现出预期的行为。

    equals方法的通用约定

    自反性:对于任何非null的引用值xx.equals(x)必须返回true
    对称性:xyz都是非null,如果x.equals(y) == true,y.equals(z) == true,那么x.equals(z) == true
    一致性:非nullxy,只要equals方法所用的对象属性没有被修改,那么多次调用x.equals(y)必定返回truefalse
    nullxx.equals(null) == false

    必须严格遵守通用规定

    没有那个类是孤立的。
    一个类的实例会被频繁地传递给另一个类的实例。很多类,包括所有的集合类,都依赖于传递给它们的对象是否遵守了equals约定。

    通用约定的详解解读——自反性

    对象必须等于自身。

    通用约定的详解解读——对称性

    任何两个对象对于它们是否相等必须保持一致。


    这个类企图与普通的字符串对象进行互操作。


    cis.equals(s) == trues.equals(cis) == false,这违反了对称性。
    把违反了equals的对称性的类的实例加入集合中,其行为是不可预测的(取决于是集合调用cis.equals(s)还是s.equals(cis))。
    一旦违反了equals约定,当其他对象面对你的对象时,你完全不知道这些对象的行为会怎么样。

    因此建议把企图与String互操作的代码从equals方法中去掉:

    通用约定的详解解读——传递性

    子类增加的信息会影响到equals的比较结果。

    扩展该类:

    直接继承Pointequals方法会忽略掉颜色信息,这是无法接受的。

    问题:


    p.equals(cp) == truecp.equals(p) == false
    解决办法:
    上面的解决方案提供了对称性,却牺牲了传递性。父类的equals方法必定适合于子类的实例。
    p1.equals(p2)==ruep2.equals(p3)==true,而p1.equals(p3)== false
    我们无法在扩展可实例化的类的时候既增加新的值组件,同时又保留equals约定。

    使用getClass测试代替instance测试:


    只有当对象具有相同的实现时,才能使对象等同。


    通过在不添加值组件的方式扩展了Point:

    里氏替换原则:一个类型的任何重要属性也将适用于它的子类型,因此为该类型编写的任何方法,在它的子类型上也应该同样运行得很好。
    CounterPoint实例传递给onUnitCircle方法,onUnitCircle方法将返回false

    通用约定地详解解读——传递性——子类添加值组件的权宜之计

    java.sql.Timestamp扩展了java.util.Date,并添加了nanoseconds域。Timestampequals实现违反了对称性,因此不可以混用TimestampDate对象。
    java.sql.Timestamp这种行为是错误的,不值得效仿。

    通用约定地详解解读——传递性——抽象类

    可以在抽象类的子类中增加新的值组件,不会违反equals约定。
    抽象类Shape,子类Circle添加radius属性,子类Rectangle添加lengthwidth属性,只要不可以直接创建超类的实例,就不会有违反传递性。

    通用约定的详解解读——一致性

    如果两个对象相等,它们就必须始终保持相等,除非它们中有一个对象(或者两个都)被修改了。
    不可变类:相等的对象永远相等,不相等的对象永远不相等。

    无论类是否可变不可变,都不要使equals方法依赖于不可靠的资源。如果违反了,想要满足一致性的要求就十分困难了。
    java.net.URLequals方法依赖于URL中主机IP地址的比较。主机是可以改变了IP的地址,因此随着时间的推移,equals不确保会产生相同的结果。

    通用约定地详解解读——非空性

    所有的对象都必须不等于null
    通用约定不允许equals方法抛出空指针异常。

    Paste_Image.png

    这项测试是不必要的。

    instanceof的第一个操作数是null,那么,不管第二个操作数是哪种类型,instanceof操作符都返回false
    因为把null传给equals方法,类型检查就会返回false,所以不需要单独的null检查。

    如何写出高质量的equals方法——使用==操作符检查“参数是否为这个对象的引用”

    优化性能

    如何写出高质量的equals方法——使用instanceof操作符检查“参数是否为正确的类型”

    正确的类型是指equals方法所在的那个类。有些情况下,是指该类所实现的某个接口。如果类实现的接口改进了equals约定,允许在实现了该接口的类之间进行比较,那么就使用接口,例如集合接口(SetListMapMap.Entry)。

    如何写出高质量的equals方法——把参数转换成正确的类型

    转换之前必须进行instanceof测试

    如何写出高质量的equals方法——对于该类中的每个“关键”域,检查参数中的域是否与该对象中对应的域相匹配

    全部测试通过,则返回true,否则返回false
    如果类型是接口,就必须通过接口方法访问该参数中的域;如果该类型是个类,也许能够直接访问参数中的域,这要取决于它们的可访问性。

    对于既不是float也不是double类型的基本类型域,可以使用==操作符进行比较;对于对象引用域,可以递归地调用equals方法;对于float域,可以使用Float.compare方法;对于double域,则使用Double.compare。对于floatdouble域进行特殊的处理是有必要的,因为存在着Float.NaN-0.0f以及类似的double常量。
    对于数组域,则要把以上这些指导原则应用到每个元素上。如果数组域的每个元素都很重要,就可以使用Arrays.equals方法。
    有些对象引用域为null是合法的,所以为了避免空指针异常,习惯使用如下的做法:

    如果fieldo.field通常是相同的对象引用,推荐使用如下的做法:

    域的比较顺序可能会影响到equals方法的性能。为了获得最佳的性能,应该最先比较最有可能不一致的域,或者是开销最低的域,最理想的情况下是两个条件同时满足的域。
    不需要比较不属于对象逻辑状态的域。
    不需要比较冗余的域,冗余域可以由“关键域”计算获得。但是比较冗余域有可能会提高equals方法的性能。如果冗余域代表了整个对象的综合描述,比较这个域可以节省当比较失败时去比较实际数据所需要的开销。

    如何写出高质量的equals方法——当你编写完了equals方法,应该问自己三个问题:它是否是对称的、传递的、一致的?

    最好编写单元测试进行测试。
    自反性和非空性通常会自动满足。

    告诫

    覆盖equals时总要覆盖hashCode
    不要企图让equals方法过于智能。
    File类不应该试图把指向同一个文件的符号链接当做相等的对象来看待。
    不要将equals声明中的Object对象替换为其他的类型。

    这是重载,不是覆盖。
    在原有的equals方法的基础上,再提供一个“强类型”的equals方法,只要这两个方法返回同样的结果,那么这是可以接受的。在特定的情况下,也许能够稍微改善性能,但是与增加的复杂度相比,这种做法是不值得的。
    推荐覆盖equals方法的时候加上@Override注解。

    相关文章

      网友评论

        本文标题:高效Java第八条覆盖equals时请遵守通用约定

        本文链接:https://www.haomeiwen.com/subject/rxwrkxtx.html