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

在Java中,覆盖Object类的equals方法时需要遵循通用约定,以确保对象间的逻辑相等性。文章讨论了何时需要覆盖equals,如值类需要确保逻辑相等,何时不需要覆盖,如对象本质上唯一。此外,还详细解释了equals方法的自反性、对称性、传递性和一致性等通用约定,强调在覆盖equals时也必须覆盖hashCode,并提供了编写高质量equals方法的指导,包括使用==操作符、instanceof检查、转换参数类型、比较域等步骤。
摘要由CSDN通过智能技术生成

原标题:高效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方法,以防止它被意外调用:

3eb386b58e6a76fa1afba005dfefdf65.png

实例受控的值类不需要覆盖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约定。 通用约定的详解解读——自反性

对象必须等于自身。 通用约定的详解解读——对称性

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

37ed1de775b9c64ada6f7cd21e1e9764.png

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

2f23a1f4cf728059b0461bfd396176bd.png

cis.equals(s) == true但s.equals(cis) == false,这违反了对称性。

a226bfaa573e0388862a0da57468c8f1.png

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

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

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

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

c744fd8f4b1a935f16585018aa642af4.png

扩展该类:

394ea29a5e1fa1f4c836bde725816deb.png

9a91a08bd2262704a7e6aac0a0eacedd.png

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

a8d341d9eec5d557519380e2f7fbad1d.png

问题:

dce5cdf9e54ad66b38078f6bd7bce42c.png

p.equals(cp) == true而cp.equals(p) == false。解决办法:

e72a20c75f809b686899a0375a65be09.png

上面的解决方案提供了对称性,却牺牲了传递性。父类的equals方法必定适合于子类的实例。

7eb7f05fc7ed39d95b93e4903c6c93db.png

p1.equals(p2)==rue,p2.equals(p3)==true,而p1.equals(p3)== false。我们无法在扩展可实例化的类的时候既增加新的值组件,同时又保留equals约定。

使用getClass测试代替instance测试:

ee93bd008546f704751a289e8b55962b.png

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

d160d235a3f822e8435cfed4a9fa9b29.png

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

337f6e450e341ab6b9dbd0c2d4559381.png

里氏替换原则:一个类型的任何重要属性也将适用于它的子类型,因此为该类型编写的任何方法,在它的子类型上也应该同样运行得很好。将CounterPoint实例传递给onUnitCircle方法,onUnitCircle方法将返回false。 通用约定地详解解读——传递性——子类添加值组件的权宜之计

29501fdc2b7361fb685ae8622f428dd2.png

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方法抛出空指针异常。

eb2936f443e883671c70029468bbfb24.png

Paste_Image.png

这项测试是不必要的。

d317657073ea165eb9bade6be8a8401c.png

9daa8f2974f3fb60320dd9b29db21ae3.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是合法的,所以为了避免空指针异常,习惯使用如下的做法:

d929fc9a795f74d2656348b461d560a3.png

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

a179d1d1c977b10a1781eb42591e4e88.png

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

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

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

09c9535252c328ec04b9b9186587d1e4.png

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

责任编辑:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值