最近在用Java写一个跟购物有关的小程序。写测试的时候遇到了判断两个对象是否相等的问题。看了些资料,说一下自己对equals()和hashCode()这两个方法的理解。
在Object类中定义了equals()和hashCode()这两个方法。Object类是类继承结构的基础,所以是每一个类的父类。所有的对象,包括数组,都实现了在Object类中定义的方法。
equals
equals()方法是用来判断其他的对象是否和该对象相等,它的性质有:
自反性(reflexive)。对于任意不为
null的引用值x,x.equals(x)一定是true。对称性(symmetric)。对于任意不为
null的引用值x和y,当且仅当x.equals(y)是true时,y.equals(x)也是true。传递性(transitive)。对于任意不为
null的引用值x、y和z,如果x.equals(y)是true,同时y.equals(z)是true,那么x.equals(z)一定是true。一致性(consistent)。对于任意不为
null的引用值x和y,如果用于equals比较的对象信息没有被修改的话,多次调用时x.equals(y)要么一致地返回true要么一致地返回false。对于任意不为
null的引用值x,x.equals(null)返回false。
对于Object类来说,equals()方法在对象上实现的是差别可能性最大的等价关系,即,对于任意非null的引用值x和y,当且仅当x和y引用的是同一个对象,该方法才会返回true。
需要注意的是当equals()方法被override时,hashCode()也要被override。按照一般hashCode()方法的实现来说,相等的对象,它们的hash code一定相等。
hashCode
hashCode()方法给对象返回一个hash code值。这个方法被用于hash tables,例如HashMap。
它的性质是:
在一个Java应用的执行期间,如果一个对象提供给equals做比较的信息没有被修改的话,该对象多次调用
hashCode()方法,该方法必须始终如一返回同一个integer。如果两个对象根据
equals(Object)方法是相等的,那么调用二者各自的hashCode()方法必须产生同一个integer结果。并不要求根据
equals(java.lang.Object)方法不相等的两个对象,调用二者各自的hashCode()方法必须产生不同的integer结果。然而,程序员应该意识到对于不同的对象产生不同的integer结果,有可能会提高hash table的性能。
大量的实践表明,由Object类定义的hashCode()方法对于不同的对象返回不同的integer。
来看一个例子。创建一个Book类:
1 2 3 4 5 6 7 8 9 | |
Book类有两个非常基础的属性:书名name和作者author。现在,来比较两本书:
1 2 3 4 5 6 7 8 9 | |
测试没有通过。虽然两本书有着同样的书名和作者,但是它们是两本“不同”的书。
关于改写
改写equals()方法看起来非常简单,但是有许多改写的方式会导致错误,并且后果非常严重。要避免问题最重要的办法是不改写equals()方法。
那么什么时候应该改写Object.equals呢?当一个类有自己特有的“逻辑相等”概念(不同于对象身份的概念),而且超类也没有改写equals()以实现期望的行为,这时需要改写equals()方法。这通常适合于value object。
再说一遍,在每个改写了equals()方法的类中,必须要改写hashCode()方法。如果不这样做,就会违反Object.hashCode的通用约定,从而导致该类无法与所有基于hash的集合类结合在一起正常运作,这样的集合类包括HashMap、HashSet和HashTable。
需要注意的是
1 2 3 | |
这个hashCode()方法是合法的,因为相等的对象总是具有同样的散列码。但是它使得每一个对象都具有同样的散列码。因此,每个对象都被映射到同一个散列桶中,从而散列表被退化为链表。对于规模很大的散列表而言,这关系到散列表能否正常工作。
一个好的散列函数通常倾向于“为不相等的对象产生不相等的散列码”。
如果一个类是非可变的,并且计算散列码的代价也比较大,那么你应该考虑把散列码缓存在对象内部,而不是每次请求的时候都重新计算散列码。
不要试图从散列码计算中排除掉一个对象的关键部分以提高性能。
我使用的是IntelliJ,使用^+N快捷键可以很好的overrideequals()和hashCode()方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | |
这个时候再跑前面的测试就会发现测试通过了。
那么如果我没有overridehashCode()会发生什么呢?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | |
这个时候你会发现之前的测试依然通过,但是如果换一下测试:
1 2 3 4 5 6 7 8 9 10 11 12 | |
逻辑上,我用相同的书在借书表里查询,应该返回对应的人名。但是这条测试并没有通过,事实上返回的是null。因为没有改写hashCode()方法,从而导致两个相等的实例具有不相等的散列码,违反了hashCode()的规定。put()方法把LiLei借出的书对象存放在一个散列桶中,get()方法会在另一个散列桶中查找LiLei所借的书对象,返回null也就不出意外了。要想修正,只要提供适当的hashCode()方法就可以了。
参考文献: