8 ADT和OOP中的“等价性”
8.1 等价关系
在物理世界中,每一个物体都是不同的——在某种程度上,即使两片雪花也是不同的,即使区别只是它们在空间中所处的位置
因此,两个物理对象永远不会真正“相等”;它们只有一定程度的相似性。
然而,在人类语言世界和数学概念世界中,同一事物可以有多个名称。
等价性问题:
软件中,什么情况下两个事物认为是等价的?可相互替代?
等价关系:自反、传递、对称
8.2 判定等价的三种方式
1…AF:抽象函数(AF)提供了一种在ADT上清晰定义相等操作的方法:如果AF映射到同样的结果,则等价
2.观察者:站在外部观察者角度看,对两个对象调用任何相同的操作,都会得到相同的结果,则认为这两个对象是等价;反之亦然。就ADT而言,“观察”意味着对对象调用操作。因此,当且仅当调用抽象数据类型的任何操作都无法区分这两个对象时,这两个对象是相等的
eg:
8.3 “==” vs“. equals()”
Java有两种不同的操作来测试相等性,它们具有不同的语义
- ==:引用等价性,在快照图中表示两个变量的箭头指向同一个对象
- .equals():对象等价性
必须为每个抽象数据类型适当定义equals操作。当我们定义一个新的数据类型时,我们的责任是决定对象相等对数据类型的值意味着什么,并适当地实现equals()操作。
注意,下图会报错:
对基本数据类型,使用==进行判定
对对象类型,使用equals()
8.4 实现equals()
8.4.1 针对不可变类型
绝大部分不可变类型都需要重写,但是需要将类继承自object类,否则会重载方法而非重写方法。编译器使用静态类型检查根据参数列表判断选择重载方法还是重写方法。编译看左,运行看右。
eg:
利用@Override
声明重写,让编译器确保签名的正确性
8.4.2 instanceof
判断某个对象是不是特定类型(或其子类型),
instanceof使用动态类型检查,不是静态类型检查
一般来说,在面向对象编程中使用instanceof不好。它最好只用于实现equals
可以使用多态(一接口+多实现类)来避免instanceof
8.5 Object类的契约
8.5.1 equals
equals方法是object类的方法,其源码如下:
public boolean equals(Object obj) {
//this表示当前对象,判断地址/对象是否相同
return (this == obj);
}
当我们使用equals函数判断对象是否等价时,需要重写equals方法,此时仍然传入object类对象,方法内部首先使用instanceof判断是否为该类类型,如果不是,直接返回false;如果是的话,则使用一个本类对象来接收传入的object的强转结果。之后,再根据需要的行为逻辑设定等价判断条件。但是需要注意等价关系的自反、传递和对称性。同时要重写hashcode方法。
//重写hashcode方法
@override
public int hashCode() {
return 31 * first.hashCode() + last.hashCode();
}
@Override
public int hashCode() {
int result = 17; // Nonzero is good
result = 31 * result + areaCode; // Constant must be odd
result = 31 * result + prefix; // " " " "
result = 31 * result + lineNumber; // " " " "
return result;
}
书写逻辑为返回result= 一个对象的hashcode*31+另一个对象的hashcode;如果有大于两个数据,则可以依次迭代,返回最后的result
8.5.2 hashcode
程序中调用同一对象的hashcode方法,都要返回相同的值;但对于多次执行程序,hashcode不要求相同
使用equals判断相同的对象必须有相等的hashcode;不相等的对象,也可以映射成相等的hashcode,但可能使程序性能变差。
8.6 可变类型的等价性
观察等价性:在不改变状态的前提下,两个可变对象是否看起来一致
行为等价性:调用对象的任何方法都展示出一致的结果
注意:对于不变的对象,观察和行为相等是相同的,因为没有任何mutator方法
对可变数据类型来说,往往更倾向于实现严格的观察等价性
- 在JDK中,不同的可变类使用不同的等价性标准
- Java对其大多数可变数据类型(如集合)使用观察相等性,但其他可变类(如StringBuilder)使用行为相等
- 如两个list中包含相同顺序的元素,则equals会返回true(观察等价性)
但有些时候,观察等价性可能导致bug,还可能破坏RI。这是因为在可变数据类型中,存在mutator,这会改变对象,产生新的hashcode,因此会影响equals和hashcode的结果,这样的话hash表的RI就会遭到破坏。
因此,如果某个可变对象包含在set集合中,当其发生改变后,其行为将变得不可测。
eg:
总结:
- 对可变类型,实现行为等价性即可
- 即只有指向同样内存空间的object,才算相等的
- 因此,对于可变数据类型来说,不需要重写equals和hashcode方法,直接继承即可
- 如果要判断两个可变对象看起来是否一致,最好重新定义一个方法。
- 不可变类型必须重写equals()和hashCode()。
eg:
object类的clone
clone()创建并返回此对象的副本。由于 Object 本身没有实现 Cloneable 接口,所以不重写 clone 方法并且进行调用的话会发生 CloneNotSupportedException 异常。
clone的确切含义可能取决于对象的类别。一般目的是,对于任何对象x:
x.clone() != x
x.clone().getclass() == x.getclass()
x.clone.equals(x)
8.7 Autoboxing and Equality
自动装箱功能:基本数据类型会在适当时候被包装成对应的对象数据类型。
两个相同的integer在(-128,127)内用“==”判断为相等,如果是new出来的对象则判断为不相等;其他情况一律视为不相等。
如果用equals则直接判断值是否相等。