介绍
三种判断等价方式
从形式上讲,我们可以通过几种方式来观察等价性。
使用抽象函数。抽象函数f:R→A将数据类型的具体实例映射到它们相应的抽象值。 我们可以使用抽象函数f作为等价的定义,a等价于b当且仅当f(a)= f(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)
使用E作为等价关系的定义,我们可以说a等价于b当且仅当E(a,b)。
使用观察。当两个对象无法通过观察进行区分时,我们可以说,这两个对象是相同的。我们可以应用的每个操作对两个对象都会产生相同的结果。 考虑集合表达式{1,2}和{2,1}。使用可用于集合的观察者操作,如基数| ... | 和成员资格∈,下面这些表达式是难以区分的:
== vs. equals()
像许多语言一样,Java有两种不同的操作,用于判断两个对象是否等价。
==运算符比较引用。 更确切地说,它测试了引用等价性。如果它们指向内存中的相同存储,则两个引用是==。 在快照图里面,如果它们的箭头指向相同的对象气泡,则两个引用是==。
equals()操作比较对象内容,换句话说,就是意义上的对象相等。
不可变类型的等价性
换句话说,equals()的默认含义与引用相等相同。 对于不可变的数据类型,这几乎总是错误的。所以必须重写equals()方法,用自己的实现代替它。
如果我们传递一个Object对象,就像在d1.equals(o2)中那样,我们最终调用equals(Object)。如果我们传递一个Duration对象,就像在d1.equals(d2)中一样,我们最终调用equals(Duration)版本。即使o2和d2都在运行时指向同一个对象,也会出现不同的结果。
下边是一个正确的equals的实现方法
对象合同
我们必须确保由equals()实现的等价定义是一个等价关系:自反,对称和传递。如果不是,那么依赖于等价性的操作将表现出不规律和不可预测的行为。
对于引用a和b,如果a== b,那么a的地址== b的地址。所以对象合同是满足的。
d1和d2是等价的,但它们有不同的哈希码,我们需要解决这个问题。
可变类型的等价性
当两个对象无法通过观察进行区分时,它们是相同的。对于可变对象,它有两种解释方法:
观察等价。当它们不能通过不改变对象状态的观察进行区分时,即只通过调用creator,producer和observer的方法无法区分两个对象。这通常被严格地称为观察平等,因为它在程序的当前状态下测试两个对象是否“看起来”相同。
行为等价。调用两个对象的任何方法,包括mutator,都无法区分两个对象。这通常被称为行为平等,因为它测试这两个对象在这个和所有未来的状态中是否会“表现”相同。
这是怎么回事? List<String>是一个可变对象。在像List这样的集合类的标准Java实现中,突变会影响equals()和hashCode()的结果。当列表首先放入HashSet中时,它将存储在当时与其hashCode()结果相对应的哈希桶中。当列表随后发生变化时,其hashCode()会发生变化,但HashSet没有意识到它应该移动到不同的存储桶中。所以它再也找不到了。
equals()和hashCode()的最终规则
对于不可变类型:
equals()应该比较抽象值。 也就是说对象是行为等价的。
hashCode()应该将抽象值映射为一个整数。
所以不可变类型必须重写equals()和hashCode()。
对于可变类型:
equals()应该比较两个引用是否指向同一个内存地址,就像==一样。
hashCode()应该将引用映射为一个整数。
所以可变类型不应该重写equals()和hashCode(),而应该简单地使用Object提供的默认实现。 不幸的是,Java不遵循这个规则,导致我们上面看到的陷阱。
总结
等价应该是一个等价关系(自反,对称,传递)。
等价和哈希码必须相互一致,以便使用哈希表(如HashSet和HashMap)的数据结构可以正常工作。
抽象函数是不变数据类型中等价的基础。
引用等价是可变数据类型中等价的基础;这是确保随时间的一致性并避免破坏散列表的不变式的唯一方法。