由于我们学校(哈工大)大二软件构造课程的大部分素材取自此,也是推荐的阅读材料之一,于是打算做一些翻译工作,自己学习的同时也能帮到一些懒得看英文的朋友。另外,该课程的阅读资料中有的练习题没有标准答案,所给出的“正确答案”为译者所写,有错误的地方还请指出。
(更新:从第10章开始只翻译正确答案)
译者:李秋豪
审校:
V1.0 Thu Apr 12 21:02:06 CST 2018
理解分别通过抽象函数、等价关系以及观察定义的“相等”。
能够辨别索引相等和对象相等的不同。
能够辨别可变类型中的观察相等和行为相等的不同。
理解“对象契约”(Object contract)并能够正确地为可变/不可变类型设计相等操作。
在之前的阅读材料中,我们已经描述了抽象数据类型(ADT)是由它对应的操作而非内部表示决定的。而ADT中的抽象函数解释了该类型是如何将内部表示映射为使用者理解的抽象数据的,我们也看到了抽象函数决定了我们应该如何实现ADT的各个操作。
在这篇阅读中我们会聚焦于如何定义ADT的相等:抽象函数会给我们对相等操作一个清晰的定义。
在现实物理世界中,任何对象都是不相等的——在某些层次,即使是两片雪花也是不同的,即使这种不同只是在空间中的位置(严格一点的话,在原子层次不能这么说,不过对于现实生活中“大”的对象已经足够正确了)。所以任何物理对象都不会真正相等,它们只会在某一些方面相似。
但是对于人类语言,或者对于数学世界,你可以有很多完全相同的东西。例如有两个相等的表达式是很正常的,又例如√9 和 3表现了完全相同的数值。
严格来说,我们可以从三个角度定义相等:
抽象函数:回忆一下抽象函数(AF: R → A ),它将具体的表示数据映射到了抽象的值。如果AF(a)=AF(b),我们就说a和b相等。
等价关系:等价是指对于关系E ⊆ T x T ,它满足:
自反性: E(t,t) ∀ t ∈ T
对称性: E(t,u) ⇒ E(u,t)
传递性: E(t,u) ∧ E(u,v) ⇒ E(t,v)
我们说a等于b当且仅当E(a,b)。
以上两种角度/定义实际上是一样的,通过等价关系我们可以构建一个抽象函数(译者注:就是一个封闭的二元关系运算);而抽象函数也能推出一个等价关系。
第三种判定抽象值相等的方法是从使用者/外部的角度去观察。
观察:我们说两个对象相等,当且仅当使用者无法观察到它们之间有不同,即每一个观察总会都会得到相同的结果。例如对于两个集合对象 {1,2} 和 {2,1},我们就无法观察到不同:
|{1,2}| = 2, |{2,1}| = 2
1 ∈ {1,2} is true, 1 ∈ {2,1} is true
2 ∈ {1,2} is true, 2 ∈ {2,1} is true
3 ∈ {1,2} is false, 3 ∈ {2,1} is false
…
从ADT来说,“观察”就意味着使用它的观察者/操作。所以我们也可以说两个对象相等当且仅当它们的所有观察操作都返回相同的结果。
这里要注意一点,“观察者/操作”都必须是ADT的规格说明中规定好的。Java允许使用者跨过抽象层次去观察对象的不同之处。例如==就能够判断两个变量是否是索引到同一个存储地方的,而 System.identityHashCode() 则是根据存储位置计算返回值的。但是这些操作都不是ADT规格说明中的操作,所以我们不能根据这些“观察”去判断两个对象是否相等。
例子: 时间跨度这里有一个不可变ADT的例子:
public class Duration { private final int mins; private final int secs; // Rep invariant: // mins >= 0, secs >= 0 // Abstraction function: // AF(min, secs) = the span of time of mins minutes and secs seconds /** Make a duration lasting for m minutes and s seconds. */ public Duration(int m, int s) { mins = m; secs = s; } /** @return length of this duration in seconds */ public long getLength() { return mins*60 + secs; } }