原标题:高效Java第八条覆盖equals时请遵守通用约定
尽管Object是一个具体类,但是设计它主要是为了扩展。它所有的非final方法(equals、hashCode、toString、clone和finalize)都有明确的通用约定,这些方法被设计成要被覆盖的。任何一个类,在覆盖这些方法的时候,都有责任遵守这些通用约定;如果不能做到这一点,其他依赖于这些约定的类就无法结合该类一起正常工作。
有许多覆盖equals方法的方式会导致错误,并且后果非常严重。最容易避免这类问题的办法就是不覆盖equals方法,在这种情况下,类的每个实例都只与它自身相等。 不需要覆盖equals方法的情况
类的每个实例本质上都是唯一的。对于代表活动实体而不是值的类来说确实如此,例如Thread。Object提供的equals实现对于这些类来说是正确的行为。不关心类是否提供“逻辑相等”的测试功能。java.util.Random可以覆盖equals,以检查两个Random实例是否产生相同的随机数序列,但是这样的功能是没有价值的。超类已经覆盖了equals,从超类继承而来的行为对于子类也是合适的。Set实现都从AbstractSet继承了equals的实现;List实现从AbstractList继承equals实现;Map实现从AbstractMap继承equals实现。
类是私有的或包级私有的,可以确定它的equals方法永远不会被调用。必须覆盖equals方法,以防止它被意外调用:
实例受控的值类不需要覆盖equals方法,因为实例受控的值类可以确保“每个值至多只存在一个对象”。例如枚举类型。实例受控的值类的实例逻辑相同与对象等同是一回事。 需要覆盖Object.equals的情况
值类——类具有自己特有的“逻辑相等”的概念(不同于对象等同),而且超类没有覆盖equals实现期望的行为,这时需要覆盖equals方法。值类:仅仅是一个表示值的类,例如Integer或Date。使用equals比较值对象的引用,是比较它们在逻辑上是否相等,而不是确认它们是否指向同一个对象。 覆盖equals方法的目的
覆盖equals方法,要让该类的实例可以做Map的键,或是Set的元素,使映射或集合表现出预期的行为。 equals方法的通用约定
自反性:对于任何非null的引用值x,x.equals(x)必须返回true。对称性:x、y、z都是非null,如果x.equals(y) == true,y.equals(z) == true,那么x.equals(z) == true。一致性:非null的x、y,只要equals方法所用的对象属性没有被修改,那么多次调用x.equals(y)必定返回true或false。非null的x,x.equals(null) == false。 必须严格遵守通用规定
没有那个类是孤立的。一个类的实例会被频繁地传递给另一个类的实例。很多类,包括所有的集合类,都依赖于传递给它们的对象是否遵守了equals约定。 通用约定的详解解读——自反性
对象必须等于自身。 通用约定的详解解读——对称性
任何两个对象对于它们是否相等必须保持一致。
这个类企图与普通的字符串对象进行互操作。
cis.equals(s) == true但s.equals(cis) == false,这违反了对称性。
把违反了equals的对称性的类的实例加入集合中,其行为是不可预测的(取决于是集合调用cis.equals(s)还是s.equals(cis))。一旦违反了equals约定,当其他对象面对你的对象时,你完全不知道这些对象的行为会怎么样。
因此建议把企图与String互操作的代码从equals方法中去掉:
通用约定的详解解读——传递性
子类增加的信息会影响到equals的比较结果。
扩展该类:
直接继承Point的equals方法会忽略掉颜色信息,这是无法接受的。
问题:
p.equals(cp) == true而cp.equals(p) == false。解决办法:
上面的解决方案提供了对称性,却牺牲了传递性。父类的equals方法必定适合于子类的实例。
p1.equals(p2)==rue,p2.equals(p3)==true,而p1.equals(p3)== false。我们无法在扩展可实例化的类的时候既增加新的值组件,同时又保留equals约定。
使用getClass测试代替instance测试:
只有当对象具有相同的实现时,才能使对象等同。
通过在不添加值组件的方式扩展了Point:
里氏替换原则:一个类型的任何重要属性也将适用于它的子类型,因此为该类型编写的任何方法,在它的子类型上也应该同样运行得很好。将CounterPoint实例传递给onUnitCircle方法,onUnitCircle方法将返回false。 通用约定地详解解读——传递性——子类添加值组件的权宜之计
java.sql.Timestamp扩展了java.util.Date,并添加了nanoseconds域。Timestamp的equals实现违反了对称性,因此不可以混用Timestamp和Date对象。java.sql.Timestamp这种行为是错误的,不值得效仿。 通用约定地详解解读——传递性——抽象类
可以在抽象类的子类中增加新的值组件,不会违反equals约定。抽象类Shape,子类Circle添加radius属性,子类Rectangle添加length和width属性,只要不可以直接创建超类的实例,就不会有违反传递性。 通用约定的详解解读——一致性
如果两个对象相等,它们就必须始终保持相等,除非它们中有一个对象(或者两个都)被修改了。不可变类:相等的对象永远相等,不相等的对象永远不相等。
无论类是否可变不可变,都不要使equals方法依赖于不可靠的资源。如果违反了,想要满足一致性的要求就十分困难了。java.net.URL的equals方法依赖于URL中主机IP地址的比较。主机是可以改变了IP的地址,因此随着时间的推移,equals不确保会产生相同的结果。 通用约定地详解解读——非空性
所有的对象都必须不等于null。通用约定不允许equals方法抛出空指针异常。
Paste_Image.png
这项测试是不必要的。
instanceof的第一个操作数是null,那么,不管第二个操作数是哪种类型,instanceof操作符都返回false。因为把null传给equals方法,类型检查就会返回false,所以不需要单独的null检查。 如何写出高质量的equals方法——使用==操作符检查“参数是否为这个对象的引用”
优化性能 如何写出高质量的equals方法——使用instanceof操作符检查“参数是否为正确的类型”
正确的类型是指equals方法所在的那个类。有些情况下,是指该类所实现的某个接口。如果类实现的接口改进了equals约定,允许在实现了该接口的类之间进行比较,那么就使用接口,例如集合接口(Set、List、Map、Map.Entry)。 如何写出高质量的equals方法——把参数转换成正确的类型
转换之前必须进行instanceof测试 如何写出高质量的equals方法——对于该类中的每个“关键”域,检查参数中的域是否与该对象中对应的域相匹配
全部测试通过,则返回true,否则返回false。如果类型是接口,就必须通过接口方法访问该参数中的域;如果该类型是个类,也许能够直接访问参数中的域,这要取决于它们的可访问性。
对于既不是float也不是double类型的基本类型域,可以使用==操作符进行比较;对于对象引用域,可以递归地调用equals方法;对于float域,可以使用Float.compare方法;对于double域,则使用Double.compare。对于float和double域进行特殊的处理是有必要的,因为存在着Float.NaN、-0.0f以及类似的double常量。对于数组域,则要把以上这些指导原则应用到每个元素上。如果数组域的每个元素都很重要,就可以使用Arrays.equals方法。有些对象引用域为null是合法的,所以为了避免空指针异常,习惯使用如下的做法:
如果field和o.field通常是相同的对象引用,推荐使用如下的做法:
域的比较顺序可能会影响到equals方法的性能。为了获得最佳的性能,应该最先比较最有可能不一致的域,或者是开销最低的域,最理想的情况下是两个条件同时满足的域。不需要比较不属于对象逻辑状态的域。不需要比较冗余的域,冗余域可以由“关键域”计算获得。但是比较冗余域有可能会提高equals方法的性能。如果冗余域代表了整个对象的综合描述,比较这个域可以节省当比较失败时去比较实际数据所需要的开销。 如何写出高质量的equals方法——当你编写完了equals方法,应该问自己三个问题:它是否是对称的、传递的、一致的?
最好编写单元测试进行测试。自反性和非空性通常会自动满足。 告诫
覆盖equals时总要覆盖hashCode。不要企图让equals方法过于智能。File类不应该试图把指向同一个文件的符号链接当做相等的对象来看待。不要将equals声明中的Object对象替换为其他的类型。
这是重载,不是覆盖。在原有的equals方法的基础上,再提供一个“强类型”的equals方法,只要这两个方法返回同样的结果,那么这是可以接受的。在特定的情况下,也许能够稍微改善性能,但是与增加的复杂度相比,这种做法是不值得的。推荐覆盖equals方法的时候加上@Override注解。