看待“相等”的三种方式
严格来说,我们可以从三个角度定义相等:
抽象函数:回忆一下抽象函数(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规格说明中的操作,所以我们不能根据这些“观察”去判断两个对象是否相等。
== vs. equals()
和很多其他语言一样,Java有两种判断相等的操作—— ==
和 equals()
。
==
比较的是索引。更准确的说,它测试的是指向相等(referential equality)。如果两个索引指向同一块存储区域,那它们就是==的。对于我们之前提到过的快照图来说,==
就意味着它们的箭头指向同一个对象。equals()
操作比较的是对象的内容,换句话说,它测试的是对象值相等(object equality)。e在每一个ADT中,quals操作必须合理定义。
对象契约
由于Object
的规格说明实在太重要了,我们有时也称它为“对象契约”(the Object Contract)。你可以在object
类中找到这些规格说明。我们在这里主要研究equals
的规格说明。当你在覆盖equals
时,要记得遵守这些规定:
equals
必须定义一个等价关系。即一个满足自反性、对称性和传递性关系。equals
必须是确定的。即连续重复的进行相等操作,结果应该相同。- 对于不是null的索引
x
,x.equals(null)
应该返回false。 - 如果两个对象使用
equals
操作后结果为真,那么它们各自的hashCode
操作的结果也应该相同。
可变类型的相等
之前我们已经对不可变对象的相等性进行了讨论,那么可变类型对象会是怎样呢?
回忆之前我们对于相等的定义,即它们不能被使用者观察出来不同。而对于可变对象来说,它们多了一种新的可能:通过在观察前调用改造者,我们可以改变其内部的状态,从而观察出不同的结果。
所以让我们重新定义两种相等:
- 观察相等:两个索引在不改变各自对象状态的前提下不能被区分。例如,只调用观察者、生产者、创建者。它测试的是这两个索引在当前程序状态下“看起来”相等。
- 行为相等:两个所以在任何代码的情况下都不能被区分,即使有一个对象调用了改造者。它测试的是两个对象是否会在未来所有的状态下“行为”相等。
对于不可变对象,观察相等和行为相等是完全等价的,因为它们没有改造者改变对象内部的状态。
对于可变对象,Java通常实现的是观察相等。例如两个不同的 List
对象包含相同的序列元素,那么equals()
操作就会返回真。