1.不可变类型的等价性
可以用AF来判断等价性。抽象函数是将R→A将数据类型的具体实例映射到相应的抽象值。如果AF映射到同样的结果,则等价。等价关系引出一个抽象函数(关系划分T,因此f将每个元素映射到它的划分类)。抽象函数引出的关系是等价关系。
站在外部观察者角度:对两个对象调用任何相同的操作,都会得到相同的结果,则认为这两个对象是等价的。当两个物体不能通过观察加以区分时,我们可以说它们是相等的——我们可以应用的每个操作对两个物体产生相同的结果。考虑集合表达式{1,2}和{2,1}。使用对集合、基数性|…|和隶属度∈可用的观察者操作,这些表达式是不可区分的:
在ADT中,“观察”意味着调用对象上的操作。因此,当且仅当不能通过调用抽象数据类型的任何操作来区分两个对象时,它们是相等的。
2.== 和. equals()
==表示的是引用等价性,即是否是同一对象、同一地址。==操作符比较引用。它测试参照相等性。如果它们指向内存中的相同存储就使用==。就快照图而言,如果两个引用的箭头指向相同的对象泡,则它们是==。equals()操作比较对象内容——换句话说,对象内容相等。所以在自定义ADT时,需要重写Object的equals()。
对基本数据类型,使用==判定相等,对对象类型,使用equals()。如果用==,是在判断两个对象是否指向内存里的同一段空间。大部分情况下想要判断的还是内容相等,故应该经常使用equals(使用前重写)。
重写equals不一定要比较所有对象拥有的属性。严格来说,在没有 AF的情况下直接在equals()中判断每个属性的等价性,是不正确的。所以大部分情况下是需要根据AF来重写equals的。此外,比较属性前应用instanceof来判断比较对象是否是该类型。
重写equals往往伴随着hashcode的变化,所以总是要在重写equals时重写hashcode()。保证equals相等时hashcode也相等即可,而hashcode相等时并不意味着equals比较相等,所以会存在哈希冲突。在重写hashcode时,我们应该有意识的避免哈希冲突,用一些单数或质数来相加会更好。
3.观察等价性和行为等价性
观察等价性是在不改变状态的情况下,两个mutable对象是否看起来一致。当它们不能通过不改变对象状态的观察来区分时,即,通过只调用观察器、生产器和构造器方法。这通常严格地称为观察性相等,因为它测试两个对象在程序的当前状态下是否“看起来”相同。而行为等价性是调用对象的任何方法都展示出一致的结果。当它们无法通过任何观察加以区分时,甚至状态也会发生变化。这种解释允许调用两个对象上的任何方法,包括mutator。这被称为行为平等,因为它测试两个对象在当前和未来的所有状态下是否会“表现”相同。对于不可变对象,观察相等和行为相等是相同的,因为没有任何mutator方法(在表示不会泄露的前提下)。
但是对可变类型来说,往往也倾向于实现严格的观察等价性。Java对大多数可变数据类型(如
集合),但其他可变类(如StringBuilder)使用行为相等。如果两个不同的List对象包含相同的元素序列,则equals()报告它们相等。但在有些时候,观察等价性可能导致bug,甚至可能破坏RI。
例如:
这个例子举得非常巧妙,在上述情况下,set中的list在外部被直接改变了,但set的哈希桶是在set一开始建立的时候就被分配好了,即使再改变其内容也不会改变已有元素的哈希桶分配,所以当list改变时,他的hashcode明明已经改变了,但是因为set没有改变哈希桶,导致无论如何搜索都不能在set中找到list了。其实有一种解决办法就是new一个set把当前的set复制过去,但是这样每次list改变的时候都要重新分配,复杂度过高,显然不是我们想要的。
所以如果某个mutable的对象包含在 HashSet集合类中,当其发生改变后,集合类的行为不确定。对可变类型,实现行为等价性即可,一般来说,这意味着当且仅当两个引用是同一对象的别名时,它们应该是equals()。所以对可变类型来说,无需重写hashcode和equals,直接继承Object的两个方法即可。若要判断内容相等的话,可提供一个新的方法similar()。
4.总结
这块的难点在于行为和观察等价性,只需要记住可变类型equals默认不需要重写,而不可变类型就没有这个烦恼,直接实现观察等价性即可。希望能对你产生帮助,欢迎讨论和提出问题。