本章主要讨论所有类的父类——Object类中的一些方法的使用,如何覆盖以及需要注意的问题
equals
方法默认的逻辑是对比两个变量所指向的是否是同一个对象(实例)。 但是如果你想对比的是逻辑相等呢?比如两个对象都含有value这个域,你只是希望根据value这个域的情况来比较对象呢? 这种对象的类被称为”值类“,比如,代表日期的类Date。 需要注意,枚举类不需要覆盖equals方法,因为枚举类每个枚举值都被限定为全局唯一的实例,因此虽然equals比较的是对象是否相同,但是对象相同保证了其所代表的值一定相同。 ### 覆盖equals必须遵守的约定 首先,覆盖equals方法必须实现如下等价关系: - 自反性: 当x不等于null时,x.equals(x)必须等于true - 对称性: 当x、y不等于null时,x.equals(y)和y.equals(x)的结果必须相同 - 传递性: 当x、y、z不等于null时,x.equals(y)和y.equals(z)都返回true,那么x.equals(z)也应该返回true - 一致性: 当x、y不等于null时,只要x,y中的相关信息没有被改变,那么无论调用多少次x.equals(y)都应该返回稳定一致的结果,如果是true就永远是true - null处理:任何对象调用equals方法和null比较都应该返回false。即x.equals(null)永远为false
如果你要覆盖equals方法,必须满足上述要求,不能偷懒,覆盖好后必须用单元测试测试上述等价关系。 书中列举除了违反上述等价关系会发生什么严重的后果,我就不一一说明了,总之就是一句话,如果不满足上述关系,你的程序可能不会像你想象的那样运行。 ### 覆盖equals时建议的步骤 覆盖equals有一些有用的技巧,建议按照下述步骤进行: 1. 优先使用==操作符检查入参是否是方法调用者的一个引用,如果是立即返回true,这是一种快速返回策略,如果后续的逻辑非常复杂而且可能会耗费性能,那么这是有必要的 2. 使用instanceof
操作符来对入参的类型做检查,如果不是方法调用者的类型或者不是类所实现的接口,那么立即返回false 3. 在上面第2步的基础上,就可以放心地将入参转换成正确的类型,不用担心抛出ClassCastException 4. 对于该类中的每个关键域,比较入参中的值和调用方法的对象中的值是否相等。 除了float和double的基本类型使用==来比较 对于float要使用Float.compare,double则使用Double.compare方法,这样特殊处理是因为有类似Float.NaN,-0.0f等特殊的常量。 对象则使用对象的equals方法来比较 如果是数组则数组中的元素要按照上述方法递归地处理 注意有些域为null是合法的,这个时候需要考虑null的情况下比较,使用三目运算符是常用的做法:field==o.field || (field != null && field.equals(o.field))
注意优先比较可能最可能不相等的域,以快速返回获得更好的性能 注意不用比较所有的域,如果有些域的值是完全由其他域得出的,那么它就是冗余的域,没必要比较这些域 5. 你编写完成了,那么你要检查一下,你写的是否满足上面提到的所有等价关系,最好写单元测试来验证它们
上面提到过这个问题,你也许会问为什么? 最好的回答是如果你只覆盖equals方法而不覆盖hashCode方法,将导致你修改的类不能够在依赖hash散列的类中正常工作,比如HashMap、HashSet、HashTable、HashXXX等。 到时候你会发现对象总是在HashMap里面神秘失踪,或者神秘出现。
hashCode返回的值主要是用在散列表中,而散列表的工作原理就是根据hashCode的返回结果将对象放在相应的位置,下次要从散列表中取出时,只要使用相同的hashCode整数值,就一定能取出相同的对象。 基于这样的工作原理,就必须要求hashCode应该满足对于同一个对象,始终返回同一个整数。 至于为什么和equals方法有关,那是因为有些散列表的实现仅仅凭借hashCode还不能确定对象的位置,仅仅凭借hashCode确定的位置里可能有多个对象,此时就需要使用equals方法来比较并得到最终的对象。HashMap的实现正是如此,先根据hashCode来得到key,value对在那个hash桶中,然后再遍历桶中的链表来获得key,value对的确切位置(使用equals方法)。
这是一个非常麻烦的技术活儿,就像书中所说,得到一个分布均匀的hash函数是非常困难的,恐怕要留给科学家们去完成,如果我们想要性能和结果非常优秀的hashCode方法,恐怕要借助于一些第三方的写好的工具库,JAVA的世界最令人赞叹的就是,当你需要什么,有人一定已经实现过了。 如果不想要这么麻烦,书中给出了一个简单的方法生成还算可以的hashCode: 1. 保存一个非零的常数值比如17到名为result的int变量中 2. 为对象中的每个关键域(就是除了冗余域的域,equals方法使用到的域)计算散列码c: 如果是boolean类型,则计算f?1:0 如果是byte、char、short、int类型,则计算(int)f 如果是long类型,则计算(int)(f^(f>>>32)) 如果是float类型,则计算Float.floatToIntBits(f) 如果是double类型,则计算Double.doubleToLongBits(f)得到long类型,然后在根据long类型得到结果 如果是对象,就看equals方法中比较这个对象时是否调用了这个对象的equals方法,如果调用了,那么这里也调用hashCode方法来得到结果,如果不是,则需要为这个域计算一个范式(我理解就是再为其实现一个类似hashCode的逻辑,将其标准化为一个数值,比如使用JSON,最终统一会得到一个string,当然JSON代价太高,只是举例),然后对范式得到的结果来调用hashCode。如果对象是null,则返回0 如果域是数组,那么数组中的每个元素都要递归地应用上面的规则,然后按照第3步的方法把结果组合起来。 3. 使用下面的公式将上面得到的结果都加进result里面: result = 31 * result + c 4. 返回result