从Scala看canEqual与正确的的equals实现

开发 后端
Java中的Equals实现,在Effective Java的描述中,是无法保证其完全正确的。不过根据Scala创始人的一篇文章,canEqual方法是可以解决这个问题的。这在Scala语言中得到了实现。

Equals实现在Java中有着很多的问题(详见《所有的Equals方法实现都是错误的》),不过这些问题并非令人完全丧气。下面通过Scala作者的一篇文章中探讨equals实现以及canEqual的使用。

在 Effective Java 中,Joshua Bloch 提到,如果一个可实例化的类定义了 equals 方法。另有一个子类继承它,也定义了额外一些属性,并且 equals 方法中需要使用这些新定义的属性进行相等性判断。那么就不可能保证 equals 语义的正确。

相信看过 Effective Java 的人当年读到这里时都会觉得丧气。就好像完美的世界突然有了一个无法缝合的裂口。先不要完全丧失兴趣,看看下面的文章:

How to Write an Equality Method in Java (51CTO曾翻译此文为《所有的Equals方法实现都是错误的》)

这篇主要由 Scala 的作者 Martin Odersky 执笔的文章中提到了一个有意思的方法。每个类在定义 equals 时,首先先判断 canEqual 能不能校验通过。canEqual 的作用就是限定:只有当被比较的对象是当前对象的子类或同类时才能通过。

  1. class Point {  
  2.  
  3.   // 属性定义  
  4.   ...  
  5.  
  6.   boolean canEqual(Object other) {  
  7.     return (other instanceof Point);  
  8.   }  
  9.     
  10.   @Override boolean equals(Object other) {  
  11.     if (other instanceof Point) {  
  12.       Point that = (Point) other;  
  13.       if (that.canEqual(this) && ...) return true 
  14.     }  
  15.     return false;   
  16.   }  
  17.     
  18. }  
  19.  

子类的定义与父类相似。

也就是说,在这样的约定下,如果拿一个父类实例和子类实例用 equals 比较肯定会返回 false。关于这篇文章,有兴趣的话可以看看相应的讨论。

讨论主要集中在文章里的方法是否违背了 Liskov Substitution Principle (LSP),以及如果违背了那么这个问题有多严重上。看过下面的分析后大家也许会觉得这种讨论没有太多意义。

我个人推荐这种 canEqual 方法。我说“方法”而不说“解决方案”是因为我觉得 Odersky 所描述的 equals 实现与 Bloch 本来所期望的 equals 逻辑模型并不一致。想像 Odersky 文章中的例子。有一个类 - 点(Point),及其子类 - 有色点(ColoredPoint)。如果一个有色点实例,其坐标与一个普遍点坐标一样,又因为有色点“是”点,所以这两点应该“相等”。大家都期望这样一个结论是成立的,所以当看到 Bloch 的结论时会觉得面向对象有其固有的自相矛盾之处。但是这样一个结论却并不是天然成立的。一个没有颜色的点与一个有颜色的点能相等吗?有人会说,如果 ColoredPoint 里面的 color 属性是一个枚举,而且那个子类被实例化成 Color.UNSPECIFIED(未指定的颜色),那么这两个点逻辑上就应该相等了吧。我认为,如果 ColoredPoint.color 可以有这样一个属性值的话,那么 Point 类就应该被定义为抽象类。Point 类此时实例化没有意义。换句话说,如果 Point 类可以实例化,且其子类 ColoredPoint 也可以有一个“未指定的颜色”,而且两者都定义了 equals,那么出现这种情况我认为是设计失败。

再看看 LSP。LSP 说,任何可以使用父类实例的地方都可以使用子类实例代替。这里并不违反 LSP,因为如果一个地方可以这样调用:

  1. Point p = new Point();  
  2. if (p.equals(...)) {  
  3.   ...  
  4. }  
  5.  

那么使用子类一样可以调用 equals。只不过,equals 在传入相同的参数时返回的结果可能会不一样。但是 LSP 并不约束必须返回一样的结果。而这正是多态的特征。

回到 Bloch 的论点上。现在赞同我的人可能会觉得 Bloch 的论点有问题。其实他说得很严谨,没有一丝问题。他的论点的前提是:可实例化的父类。也就是说无法针对非抽象类写出满足大家传统期望的子类。只不过,另人失望地,他在提出这个结论后没有给出对应的方法。相对来说,Odersky 理清了 Bloch 的逻辑模型。所以,在 Odersky 所发明的 Scala 中,canEqual 这个方法也被作为官方推荐的 equals实现方法。

【编辑推荐】

  1. Java:所有的equals方法实现都是错误的?
  2. Java语言中深入研究Java equals方法
  3. Java是平台而非产品:可添加型概念需改变
  4. Java中的堆内存与栈内存分配浅析
  5. Scala编程语言
责任编辑:yangsai 来源: JavaEye博客
相关推荐

2009-09-22 09:42:24

Scala的核心

2009-12-09 09:15:47

从Java走进ScalTwitter API

2021-06-26 07:04:24

Epoll服务器机制

2009-06-15 15:33:13

ScalaTwitter

2009-02-04 17:32:03

ibmdwJavaScala

2021-05-06 10:33:30

C++Napiv8

2017-04-05 20:00:32

ChromeObjectJS代码

2024-03-07 13:30:44

Java对象true

2014-04-22 09:51:24

LongAdderAtomicLong

2021-07-07 23:38:05

内核IOLinux

2020-03-09 15:40:27

RSACDevSecOps悬镜安全

2009-07-06 15:55:50

2009-04-28 18:32:54

2009-06-16 17:54:38

Scala类语法语义

2013-08-27 18:31:33

恒天然SPC软件

2009-08-21 16:17:25

ScalaTwitter API

2021-06-18 06:02:24

内核文件传递

2015-06-15 18:44:15

Apple Watch微游戏

2009-09-28 11:01:39

从Java走进Scal

2009-06-30 09:31:53

点赞
收藏

51CTO技术栈公众号