第八章讲述了ADT和OOP中的等价关系,以及不同的等价判别方式。通过了解不同的等价定义以及equals和hsahCode等方法的机制,来为ADT的实现选择合适的等价判断逻辑。
(一)等价关系
自反、传递、对称。比如“差值小于5即等价”不能算作等价关系,因为不满足传递性。
(二)等价的三种类型
AF判定:若a等价于b当且仅当AF(a)等价于AF(b),则等价
观察判定:若在外部观察者的角度上对两个对象调用任何相同操作都会得到相同的结果,则等价。
(三)“==”与equals
==为引用等价性,当两者指向同一个内存位置时才等价。对基本数据类型可用==判定等价。
equals一般是判断object的内容是否一致,称之为对象等价性。可以被重写。对于对象类型,应总是使用equals判定等价。
(四)实现equals
Object中默认的equals使用的是==来直接判断,因此大多数的类需要重写equals,比如通过下图的重写来从引用等价性改为对象等价性(实际上有一部分属于重载)。
由于重载进行静态检查,所以存在以下结果:
尽管运行时o2和d2是一个结果,但编译时就已经决定了调用的方法不同,判断规则也就不同。
equals重写时可以使用instanceof判断某个对象是否为某类或其子类型,但是属于动态类型检查,一般不在equals之外使用(类似getClass方法)。
(五)Object类的契约
重写equals时必须遵循以下合约:
自反传递对称的等价关系,多次调用equals应在不修改对象的条件下返回相同的结果,x.equals(null)应为假,等价对象的hashCode结果必须相同。
即自反性、传递性、对称性、持续性(结果不变)、非空性,以及对类的所有对象都适用。
hash table:哈希表包含一个数组,键值对中的key被映射为hashCode,对应数组的索引,决定了数据存储在数组的哪个位置。
有时会发生两个键映射到同一个hashcode,这时对应位置储存的值会以类似于链表的结构向后延伸。因此不相等的对象也可映射到同一个hashcode,但是多次调用同一对象的hashcode必须相同,而且equals为真的对象必须有相同的hashcode。
默认情况下在Object类中,equals使用==来判断等价,而hashCode则返回类的内存地址。因此只重写equals不够,还需要重写hashCode,可以让类的所有对象的hashcode为同一个常量(不推荐,效率低)。
一般重写会让hashCode使用equals中调用的量的hashCode进行重新组合,比如:
或者:
(六)可变类型的等价
可变类型的等价包括两种:观察等价性(不变时看起来相等)和行为等价性(调用对象任何方法结果都相同)
可变类型一般使用观察等价性,比如List只要有相同顺序的元素就等价。但观察等价性可能导致bug
当进行如下操作后
只是修改了List元素后,set的contain发生了错误的判断,这是因为List第一次被放入hashset时,它根据其hashcode被存放在哈希桶hashbucket中,当List的hashcode变化后,hashset却没有更新它在桶中的位置,导致查找时按照新的hashcode是找不到内容的。因此当equals和hashcode的结果可能被可变影响时,hash table的RI 会遭到破坏。
因此对于可变类型可以只实现行为等价性,必须指向同一个位置才等价。对于可变类型来说无需重写hashCode和equals,继承Object即可。如果一定要判断观察等价性,可以重新设计一个新的方法。
总而言之:
(七)自动封装和等价
基本数据类型可以通过自动封装转为对应数据类,转换后对其进行判断时使用的是对应数据类的equals而非==。
而在-128到127之间的整数类Integer都视作位于同一地址空间,对它们的引用等价性(==)判断都是真,但new关键字可无视此条规则。如下图左假右真。