HIT软件构造:ADT和OOP中的“等价性”

目录

1.等价关系

2.不可变类型的等价

2.1使用 AF 定义等价

2.2使用“观察”定义等价

3. == vs .equal()

4.实现 equal()

5.The contract of equals() in Object

5.1 equal()定义一个等价关系

5.2 “相等对象"hashCode()的结果一致

6.可变类型的等价性

7.总结:Rule for equals() and hashCode()

7.1对于immutable类型

7.2对于mutable类型


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 定义等价

abstraction function (AF)f: R → A:将数据类型的具体实例映射到它们对应的抽象值。
使用抽象函数 f 作为等价的判定,我们假设 a 与 b 等价当且仅当 f(a) = f(b) .也就是说AF映射到同样的结果,则等价
一个等价关系引出一个抽象函数(这个等价可以将T分区,因此 f 将每个元素映射到它对应的T的分区)。由抽象函数引起的关系是一种等价关系。

2.2使用“观察”定义等价

"观察":客户可以观察到的情况。
就ADT而言,“观察”:是指调用对对象的操作。因此,当且仅当它们不能通过调用抽象数据类型的任何操作来区分时,两个对象才相等。
“使用观察”:当两个对象不能通过观察来区分时,我们可以说它们是相等的——我们可以应用的每一个操作都会对两个对象产生相同的结果。
站在外部观察者角度:对两个对象调用任何相同的操作,都会得到相同的结果,则认为这两个对象是等价的。反之亦然!

例如:考虑集合表达式 {1,2} 和 {2,1} 。使用可用的观察器操作,基数|…|和成员∈,这些表达式无法区分:
• |{1,2}| = 2 and |{2,1}| = 2
• 1 ∈ {1,2} is true, and 1 ∈ {2,1} is true
• 2 ∈ {1,2} is true, and 2 ∈ {2,1} is true
• 3 ∈ {1,2} is false, and 3 ∈ {2,1} is false
因此,就观察而言,集合表达式 {1,2} 和 {2,1}是等价的。

3. == vs .equal()

Java有两个不同的操作来测试相等性,它们使用不同的语义。

 '==' 操作符会比较引用。它测试引用等价性。如果两个引用指向内存中的相同地方,则它们是'==' 的。在snapshot diagrams中,如果两个引用的箭头指向同一个对象圈,则这两个引用是’==‘的。
对于基本数据类型,使用 == 判定相等。

.equal()操作会比较对象的内容——换句话说,就是对象等价性。
对于对象类型,使用equal(),判定相等。如果用==,是在判断两个对象身份标识 ID是否相等(指向内存里的同一段空间)

当我们定义一个新的数据类型时,我们有责任决定对象相等对于数据类型的值意味着什么,并适当地按我们定义的相等意义实现 equal() 操作。即在自定义ADT时,需要重写Object的equals()。

4.实现 equal()

equal()方法由Object定义,其默认实现如下所示:

在Object中实现的缺省equals()是在判断引用等价性,这通常不是程序员期望的,因此,我们必须重写equal()方法,用我们自己的实现替换它。

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()的结果一致

哈希表是映射的表示形式:将键映射到值的抽象数据类型。
哈希表提供了恒定的时间查找,因此它们往往比树或列表表现得更好。键不需要被预置,或有任何特定的属性,除了提供equalhashCode
哈希表工作:(1).它包含一个数组,其初始化的大小对应于我们期望插入的元素的数量;(2).当出现一个键和一个值需要插入时,我们计算该键的hashCode,并将其转换为数组范围内的索引(例如,通过模除法)。然后将该值插入到该索引中。

因此,如果两个对象根据equal()方法相等,那么对这两个对象分别调用hashCode方法必须产生相同的整数结果。 但,不相等的对象,也可以映射为同样的hashCode,但性能会变差。
为什么对象契约要求等价的对象具有相同的哈希码?

(1).如果两个相等的对象有不同的散列码,它们可能会被放置在不同的插槽中。
(2).因此,如果试图使用key equal来查找值,则查找可能会失败。
Object的默认hashCode()实现与其默认equals()一致:
因此,当我们重写equals()时,也需要重写hashCode()。除非能保证ADT不会被放入到Hash类型的集合类中。

 重写hashCode():
(1).一个简单而激烈的方法是,hashCode总是返回一些常量值,因此每个对象的哈希代码都是相同的。但它会产生灾难性的性能影响,因为每个键都将存储在同一个槽中,并且每个查找都将退化为沿着长列表的线性搜索。
(2).标准是用于确定相等式的对象的每个组件计算哈希代码(通常通过调用每个组件的hashCode方法),然后将这些组件组合起来,加入一些算术运算。例如:

6.可变类型的等价性

观察等价性(observational equality):在不改变状态的情况下,两个mutable对象是否看起来一致。
行为等价性(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类型

equals()应该比较抽象值。这就等于说equals()应该提供行为等价性。
hashCode()应该将抽象值映射到一个整数。
因此,immutable类型必须同时重写equals()和hashCode()。

7.2对于mutable类型

equals()应该比较引用,就像’==‘一样。同样,这就等于说equals()应该提供行为等价性。
hashCode()应该将引用映射为一个整数。
因此,mutable类型根本不用重写equals()和hashCode(),而应该只需简单地使用Object提供的默认实现。不幸的是,Java的collections没有遵循这个规则,这导致了我们在上面看到的陷阱。

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值