目录
5.The contract of equals() in Object
7.总结:Rule for equals() and hashCode()
1.等价关系
等价关系:自反、对称、传递。
一个等价关系 E ⊆ T x T :
– reflexive(自反): E(t,t) ∀t∈T
– symmetric(对称): E(t,u) ⇒ E(u,t)
– transitive(传递): E(t,u) ∧ E(u,v) ⇒ E(t,v)
对于像==或equals()这样的布尔值二进制操作,等价值E是对操作返回true的成对(x,y)的集合。
2.不可变类型的等价
2.1使用 AF 定义等价
使用抽象函数 f 作为等价的判定,我们假设 a 与 b 等价当且仅当 f(a) = f(b) .也就是说AF映射到同样的结果,则等价
2.2使用“观察”定义等价
"观察":客户可以观察到的情况。
就ADT而言,“观察”:是指调用对对象的操作。因此,当且仅当它们不能通过调用抽象数据类型的任何操作来区分时,两个对象才相等。
“使用观察”:当两个对象不能通过观察来区分时,我们可以说它们是相等的——我们可以应用的每一个操作都会对两个对象产生相同的结果。站在外部观察者角度:对两个对象调用任何相同的操作,都会得到相同的结果,则认为这两个对象是等价的。反之亦然!
因此,就观察而言,集合表达式 {1,2} 和 {2,1}是等价的。
3. == vs .equal()
Java有两个不同的操作来测试相等性,它们使用不同的语义。
'==' 操作符会比较引用。它测试引用等价性。如果两个引用指向内存中的相同地方,则它们是'==' 的。在snapshot diagrams中,如果两个引用的箭头指向同一个对象圈,则这两个引用是’==‘的。
对于基本数据类型,使用 == 判定相等。
.equal()操作会比较对象的内容——换句话说,就是对象等价性。
对于对象类型,使用equal(),判定相等。如果用==,是在判断两个对象身份标识 ID是否相等(指向内存里的同一段空间)
当我们定义一个新的数据类型时,我们有责任决定对象相等对于数据类型的值意味着什么,并适当地按我们定义的相等意义实现 equal() 操作。即在自定义ADT时,需要重写Object的equals()。
4.实现 equal()
equal()方法由Object定义,其默认实现如下所示:
equal()重写示例:
instanceof 操作符将测试对象是否为特定类型的实例。使用“ instanceof”是动态类型检查,而不是静态类型检查。但是,一般来说,在面向对象编程中使用实例是一件坏事。除了实现equal()之外,任何地方都不允许它。
我们可以使用多态性来避免使用 instanceof:
5.The contract of equals() in Object
5.1 equal()定义一个等价关系
equal()方法定义了一个等价关系:自反、对称、传递,任何相等的对象必须满足等价关系,除非对象被修改了,否则调用多次equals应同样的结果。
例如:如下的equal()违反了等价关系中的传递性,若按下列equal()逻辑判断,d_0_57等价于d_0_60,d_0_60等价于d_1_03,但d_0_57与d_1_03不等价。
5.2 “相等对象"hashCode()的结果一致
哈希表是映射的表示形式:将键映射到值的抽象数据类型。
哈希表提供了恒定的时间查找,因此它们往往比树或列表表现得更好。键不需要被预置,或有任何特定的属性,除了提供equal和hashCode。
哈希表工作:(1).它包含一个数组,其初始化的大小对应于我们期望插入的元素的数量;(2).当出现一个键和一个值需要插入时,我们计算该键的hashCode,并将其转换为数组范围内的索引(例如,通过模除法)。然后将该值插入到该索引中。
因此,如果两个对象根据equal()方法相等,那么对这两个对象分别调用hashCode方法必须产生相同的整数结果。 但,不相等的对象,也可以映射为同样的hashCode,但性能会变差。
为什么对象契约要求等价的对象具有相同的哈希码?
(1).如果两个相等的对象有不同的散列码,它们可能会被放置在不同的插槽中。
(2).因此,如果试图使用key equal来查找值,则查找可能会失败。
Object的默认hashCode()实现与其默认equals()一致:
因此,当我们重写equals()时,也需要重写hashCode()。除非能保证ADT不会被放入到Hash类型的集合类中。
重写hashCode():
(1).一个简单而激烈的方法是,hashCode总是返回一些常量值,因此每个对象的哈希代码都是相同的。但它会产生灾难性的性能影响,因为每个键都将存储在同一个槽中,并且每个查找都将退化为沿着长列表的线性搜索。
(2).标准是用于确定相等式的对象的每个组件计算哈希代码(通常通过调用每个组件的hashCode方法),然后将这些组件组合起来,加入一些算术运算。例如:
6.可变类型的等价性
行为等价性(behavioral equality):调用对象的任何方法都展示出一致的结果。
(对于不可变对象,观察和行为等价性是相同的,因为没有任何突变方法。)
Java对其大多数可变数据类型,往往倾向于实现严格的观察等价性如Collections),但其他可变数据类(如StringBuilder)使用行为等价性。但在有些时候,观察等价性可能导致bug,甚至可能破坏RI。
为何会产生这样的结果?
List是一个可变的对象。在List等集合类的标准Java实现中,突变会影响equals()和hashCode()的结果。当该List第一次被放入哈希集时,它被存储在与当时的hashCode()结果对应的哈希桶/散列桶中。当列表随后发生突变时,它的HashCode()会改变,但是HashSet并没有意识到它应该被移动到另一个桶中。所以它就再也找不到了。
当等于()和hashCode()可能受到突变的影响时,我们就会破坏使用该对象作为键的哈希表的代表不变量。
所有,如果某个mutable的对象包含在Set集合类中,当其发生改变后,集合类的行为不确定, 务必小心。
上述总结:
对可变类型,实现行为等价性即可;一般来说,只有指向同样内存空间的objects,才是相等的。所以对可变类型来说,无需重写这两个函数,直接继承Object的两个方法即可。如果一定要判断两个可变对象看起来是否一致,最好定义一个新的方法。
7.总结:Rule for equals() and hashCode()
7.1对于immutable类型
– hashCode()应该将抽象值映射到一个整数。
– 因此,immutable类型必须同时重写equals()和hashCode()。
7.2对于mutable类型
– hashCode()应该将引用映射为一个整数。
– 因此,mutable类型根本不用重写equals()和hashCode(),而应该只需简单地使用Object提供的默认实现。不幸的是,Java的collections没有遵循这个规则,这导致了我们在上面看到的陷阱。