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

尽管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注解。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容