《深入理解Scala》——第2章,第2.5节多态场景下的判等

2.5 多态场景下的判等
深入理解Scala
众所周知,为多态的面向对象系统定义合适的判等和散列方法是个特别难的过程。这是因为子类可能在整个过程中造成一些相当怪异的问题,尤其是当类型层次上有多个实体(concrete)级别的时候。一般来说,对于需要比引用判等更强的判等(译者注:比如需要判断对象内部数据)的类,最好避免多层实体类层次。这是什么意思呢?有些时候类只需要引用判等就够了。也就是说只要两个对象不是同一个实例就判为不等。但是如果我们需要判断两个不同实例是否相等,而且又有多层实体类层次(multiple concrete hierarchies),那我们在实现判等的时候就要特别小心了。
为了理解这个问题,我们来看下如何写一个好的判等方法。为此,我们从写一个显示和渲染时间线和事件的库开始。

2.5.1 例子:时间线库
我们想构建一套时间线,或称日历构件。这个构件需要显示日期、时间、时间安排,以及每天相关的事件。这个库的基础概念叫作一个瞬时(a instantaneoustime)。
我们用InstantaneousTime类表示时间序列中一个特定的时间片。我们本可以用java.util.Date类,但是我们更希望使用某种具有不变性的东西,因为我们刚刚学到了不变性使写好的equals和hashCode方法变得简单。为了简化例子,我们把时间保存为返回自1970年1月1日00:00:00 GMT以来的秒数(译者注,java.util.Date是毫秒数)。我们假定所有的其他时间都能格式化为这种形式的表示,而且时区和表现形式是正交的不同问题。我们还对应用中关于判等的使用做如下的一般假设。
• 如果调用equals返回true,这是因为两个对象是同一个引用。
• 大部分对equals的调用返回false。
• 我们实现的hashCode足够稀疏,对于大部分判等比较,hashCode会是不同的。
• 计算散列值比做一次深度判等比较的效率高。
• 引用判等比做一次深度判等比较的效率高。
上述假设是大部分判等实现的标准假设。但对你的应用来说不一定始终正确。我们现在初步实现这个类和简单的equals、hashCode方法,看下是什么样子。

这个类只有一个成员,repr,是个整数,表示自1970年1月1日00:00:00 GMT以来的秒数。因为repr是这个类里唯一的数据值,并且它具有不变性,equals和hashCode方法就基于这个值来实现。在JVM里实现equals时,一般来说在做深度判等前先判断引用是否相等的性能更高。但是在这个例子里就没必要这么做了。对于有一定复杂度的类来说,这么做(先判断引用相等)能够极大地提高性能,然而这个类太简单,真没必要这么做。设计好的equals方法的另一个常用范式是(在深度判等之前)用hashCode做个早期判断。在散列值足够稀疏且易于计算的情况下,这是一个好主意。跟引用判等一样,在当前这个例子里不是很需要这么做,但对于一个足够复杂的类来说,性能会高很多。
这个类告诉我们两个道理:① 好的判等方法很重要。② 你应该经常挑战代码里的假定条件。在这个例子里,按照“最佳实践”实现的判等方法,尽管对于足够复杂的类非常有用,但对于我们这个简单的类就几乎没上面好处。
注意:在给自己的类实现判等方法时,确认一下标准的判断实现方式中的一些假设对你的类是否适用。
我们的equals实现还有一个瑕疵,那就是多态。

2.5.2 多态判等实现
一般来说,最好避免在需要深度判等的情况下使用多态。Scala语言自身就出于这个原因不再支持case class的子类继承。然而,还是有些时候这样做是有用甚至是必要的。要做到这一点,我们需要确保正确地实现了判等比较,把多态放在脑子里,并且在方案中利用多态。
我们来实现一个InstantaneousTime的子类,这个子类比父类多保存了标签(label)。我们在时间线上保存时间的时候使用这个类,所以我们就叫它Event。我们假定同一天的事件被散列到同一个桶里,因此具有相同的散列值。但是判等则还要检查事件的名字是否相等。我们快速地实现一个。

我们抛弃了之前代码里的hashCode早期检测,因为在我们这个特定的案例里,检测repr的值性能是一样的高。你会注意到的另一件事是我们修改了模式匹配,使得只有两个Event对象才能做判等。我们在REPL里试用一下。

发生什么事了?旧的类使用旧的判等方法,因此没检查新的name字段,我们需要修改基类里最初的判等实现,以便考虑到子类可能希望修改判等的实现方法。在Scala里,有个scala.Equals特质能帮我们修复这个问题。Equals特质定义了一个canEqual方法,可以和标准的equals方法串联起来用。通过让equals方法的other参数有机会直接造成判断失败,canEqual方法使子类可以跳出(opt-out)其父类的判等实现。为此我们只需要在我们的子类里覆盖canEqual方法,注入我们想要的任何判断标准。
在考虑到多态的情况下,我们用这两个方法来修改我们的类。

我们做的第一件事是在InstantaneousTime里实现canEqual,当other对象也是一个InstantaneousTime时返回true。然后我们在equals实现里考虑到other对象的canEqual结果。最后,Event类里覆盖canEqual方法,使Event只能和其他Event做判等。
作者注:在覆盖父类的判等方法时,同时覆盖canEqual方法。
canEqual方法是个控制杆,允许子类跳出父类的判等实现。这样子类就可以安全地覆盖父类的equals方法,而避免父类和子类的判等方法对相同的两个对象给出不同的结果。
我们来看下之前的REPL会话,看看新的equals方法是否有所改善。

我们成功地定义了恰当的判等方法。我们现在可以写出一般情况下通用的equals方法,也可以正确处理多态场景了。

时间: 2024-08-24 03:51:15

《深入理解Scala》——第2章,第2.5节多态场景下的判等的相关文章

《深入理解Scala》——导读

目 录 第1章 Scala--一种混合式编程语言1.1节Scala一种混合式编程语言 1.2 当函数式编程遇见面向对象 1.3 静态类型和表达力 1.4 与JVM的无缝集成 1.5 总结 第2章 核心规则 2.1 学习使用Scala交互模式(REPL)2.2 优先采用面向表达式编程 2.3 优先选择不变性2.4 用None不用null2.5 多态场景下的判等2.6 总结 第3章 来点样式-编码规范 第4章 面向对象编程 第5章 利用隐式转换写更有表达力 第6章 类型系统 第7章 隐式转换和类型系

《深入理解Scala》——第1章,第1.4节与JVM的无缝集成

1.4 与JVM的无缝集成 深入理解Scala Scala的吸引力之一在于它与Java和JVM的无缝集成.Scala与Java有很强的兼容性,比如说Java类可以直接映射为Scala类.这种紧密联系使Java到Scala的迁移相当简单,但在使用Scala的一些高级特性时还是需要小心的,Scala有些高级特性是Java里没有的.在Scala语言设计时已经小心地考虑了与Java无缝交互的问题,用Java写的库,大部分可以直接照搬(as-is)到Scala里. 1.4.1 Scala调用Java 从S

《深入理解Scala》——第2章,第2.1节学习使用Scala交互模式(REPL)

第2章 核心规则深入理解Scala 本章包括的内容: • 使用Scala交互模式(Read Eval Print Loop 简称REPL) • 面向表达式编程 • 不变性(Immutability) • Option类 本章内容覆盖了每个新Scala开发者都需要知道的几个主题.本章不会深入到每个主题里,但是会讲到可以让你自己去接着探索的程度.你将学会使用REPL,学会如何利用这个工具做软件的快速原型开发.然后我们会学到面向表达式编程,并从另一个视角来看控制结构是怎么回事.在此基础上,我们来研究不

《深入理解Scala》——第1章,第1.3节静态类型和表达力

1.3 静态类型和表达力 深入理解Scala 开发人员中有一个误解,认为静态类型必然导致冗长的代码.之所以如此是因为很多继承自C的语言强制要求程序员必须在代码中多处明确地指定类型.随着软件开发技术和编译器理论的发展,情况已经改变.Scala利用了其中一些技术进步来减少样板(boilerplate)代码,保持代码简洁. Scala做了以下几个简单的设计决策,以提高代码表达力. • 把类型标注(type annotation)换到变量右边. • 类型推断. • 可扩展的语法. • 用户自定义的隐式转

《深入理解Scala》——第1章,第1.5节总结

1.5 总结 深入理解Scala 本章中,你学到了一些Scala的设计理念.设计Scala的初衷在于把不同语言中的多种概念融合起来.Scala融合了函数式和面向对象编程,尽管显然Java也已经这么做了.Scala精选其语法,极大地减少了语言中的繁冗之处,使一些强大的特性可以优雅地表达,比如类型推断.最后,Scala和Java能够紧密集成,而且运行在Java虚拟机上,这或许是让Scala变成一种实用选择的最重要的一点.几乎不花代价就可以把Scala用于我们的日常工作中. 因为Scala融合了多种概

《深入理解Scala》——第1章,第1.1节Scala一种混合式编程语言

第1章 Scala--一种混合式编程语言 Scala是一种将其他编程语言中的多种技巧融合为一的语言.Scala尝试跨越多种不同类型的语言,给开发者提供面向对象编程.函数式编程.富有表达力的语法.静态强类型和丰富的泛型等特性,而且全部架设于Java虚拟机之上.因此开发者使用Scala时可以继续使用原本熟悉的某种编程特性,但要发挥Scala的强大能力则需要结合使用这些有时候相互抵触的概念和特性,建立一种平衡的和谐.Scala对开发者的真正解放之处在于让开发者可以随意使用最适合手头上的问题的编程范式.

《深入理解Scala》——第2章,第2.6节总结

2.6 总结 深入理解Scala 本章中我们了解了Scala编程时的第一个关键组成部分.利用REPL做快速原型是每个成功的Scala开发者必须掌握的关键技术之一.面向表达式编程和不可变性都有助于简化程序和提高代码的可推理性.Option也有助于可推理性,因为它明确声明了是否接受空值.另外,在多态的场景下实现好的判等可能不容易.以上这些实践可以帮助我们成功踏出Scala开发的第一步.要想后面的路也走得顺利,我们就必须来看一下编码规范,以及如何避免掉进Scala解析器的坑.

《深入理解Scala》——第1章,第1.2节当函数式编程遇见面向对象

1.2 当函数式编程遇见面向对象 深入理解Scala 函数式编程和面向对象编程是软件开发的两种不同途径.函数式编程并非什么新概念,在现代开发者的开发工具箱里也绝非是什么天外来客.我们将通过Java生态圈里的例子来展示这一点,主要来看Spring Application framework和Google Collections库.这两个库都在Java的面向对象基础上融合了函数式的概念,而如果我们把它们翻译成Scala,则会优雅得多.在深入之前,我们需要先理解面向对象编程和函数式编程这两个术语的含义

《深入理解Scala》——第2章,第2.2节优先采用面向表达式编程

2.2 优先采用面向表达式编程 深入理解Scala 面向表达式编程是个术语,意思是在代码中使用表达式而不用语句.表达式和语句的区别是什么?语句是可以执行的东西,表达式是可以求值的东西.在实践中这有什么意义呢?表达式返回值,语句执行代码,但是不返回值.本节我们将学习面向表达式编程的全部知识,并理解它对简化程序有什么帮助.我们也会看一下对象的可变性,以及可变性与面向表达式编程的关系. 作者注:语句VS表达式 语句是可以执行的东西,表达式是可以求值的东西. 表达式是运算结果为一个值的代码块.Scala