在很多场景下,需要判定两个对象是否“相等”,例如,判断某个Collection中是否包含特定元素。
“==”和“equals()”有何区别?如何定义ADT正确实现equals()
一. 什么是等价性(Equality)
ADT是对数据的抽象,体现为一组对数据的操作。而抽象函数AF是将内部表示R转换为抽象表示A。而等价性就是基于AF来定义的。
现实中每个对象都是独特的,所以无法有完全相等,但有“相似性”,但是在数学中,“绝对相等”是存在的
二. 三个判断等价性的方法
1. 使用AF来判定是否等价——如果AF映射到同样的结果,那么二者等价
2. 等价关系(满足自反、传递、对称)
-这二者实际上是相同的
· 等价关系引出了一个抽象函数
· 抽象函数所引起的关系是等价关系
3. 客户端所观察到的结果是相同的,则二者等价。
e.g. 考虑两个集合{1, 2}和 {2, 1}。这二者在客户端来看显然是等价的。但是其内部的表示可能不完全相同
e.g.
如果根据AF来判断,只有d1和d4是等价的,但是如果站在客户端角度来看,调用getLength这个观察器,d1,d3,d4的结果都是相同的,所以三者也是相同的。
三. ‘==’ vs. equals()
==:在Java之中这是用来判断引用等价性(referential equality)
equals():在Java之中这是用来判断对象等价性(object equality)
而对于不同的ADT时,判断相等的条件也不尽相同,因此需要重写equals()
对于基本数据类型,一般使用==来判断相等;
但是对于对象数据类型,使用equals()来判断相等。
· 如果使用==,是在判断两个对象的身份标识ID是否相同(指向内存之中同一段空间)
· 最好使用equals()来判断
在对象引用之中使用==是一种糟糕的选择
e.g.
四. 不可变类型的等价性
在Object之中默认的equals()是在判断引用等价性。但是这通常不是程序员所期望的,因此需要重写。
e.g.
我们要判断d1和d2、o2的等价关系,要针对Duration类之中的equals来判断。很显然,Duration之中有两个equals(),一个的参数是Duration类,另一个是Object类,这是因为Duration是Object的子类,它继承了Object类之中的equals(),而Duration中的equals()是对Object类中的equals()的重载。
因此在d1调用equals来判断时,会在运行时发生动态分派。d1调用的equals()传递的参数类型是Object则会调用下面的equals(),如果是Duration类的则会调用上面的equals()。因此结果是
instanceOf()——这是用来判断一个对象是不是某一个类型的操作。对于instanceOf的操作是一个动态类型检查,而不是静态类型检查。通常情况下,在OOP之中使用instanceOf是一个糟糕的选择,除了实现equals之外,它在任何地方都应该被禁止。这种对于探测对象运行时的类型的操作的禁止也包括getClass()
五. Object的“合同”
equals():当重写equals()时,必须遵守它的一般规则
· equals必须是一个等价关系——它满足自反的,对称的,传递的
· equals应当是一致的(consistent)——对于equals的多次调用的结果应当是一样的
· 对于空指针(null)的引用所调用的equals必须返回false
· 如果两个对象相同,那么它们的hashCode也必须相同
- hashCode确定了不同对象在哈希表之中的位置,而为了提高哈希表的效率,哈希表之中的元素最好均匀分布,即不同对象hashCode最好不相同,但是相同的对象,它们的hashCode必须相同,不然在哈希表之中的位置就不是同一个位置。
e.g.
- 因此在重写equals的时候一定要重写hashCode
六. 可变类型的等价性
观察等价性(observational equality):在不改变对象状态的情况下,两个mutable对象看起来是否一致
行为等价性(behaviorial equality):调用对象的任何方法都展现出一致的结果
注意:不可变类型之中观察等价性和行为等价性是相同的,因为不可变类型没有变值器(Mutator)
对于可变类型来说,往往倾向于实现严格的观察等价性
但在有些时候,观察等价性可能导致bug,甚至可能破坏RI
e.g.
然后我们判断set之中是否含有list
接下来我们在list之中添加一个新的元素
然后在判断set之中是否含有list
原因是:在list调用了add这个Mutator之后,它的hashCode发生了变化,但是存放它的哈希表并没有意识到要将这个list放到一个新的地方,因此就无法再次根据哈希表来查找这个list。
用一个更形象的例子来说明:你在派出所申领了身份证,留了当时的照片;几个月以后,你“整容”了(mutated),你用乘机的时候就无法匹配到你的身份证照片了。
在Java之中Collections是使用观察等价性,但是其它可变类(像是StringBuilder)是使用行为等价性。
· 对可变类型,实现行为等价性即可;也就是说只有指向相同的内存空间才是相等的。
· 所以对可变类型来说,无需重写这两个函数,直接继承Object的方法即可
· 如果一定要判断两个可变类型是否相等,最好定义一个新的方法
七. 自动封装(Autoboxing)和等价性
基本数据类型和他们的对象数据类型是等价的 e.g. int 和 Integer
但是x==y -> false;因为==是引用等价性
但是对于(int)x == (int) y -> true
e.g.
显然最后的判断是false,因为a.get()所返回的类型是Integer,而==是比较引用等价性,所以是false。
资料来源 MIT6.031 哈工大软件构造课程