编写一个类的时候,覆盖equals方法的同时也要覆盖hashCode,这是常识。覆盖equals比较简单,如基本类型直接比较,引用类型则递归调用equals即可。覆盖hashCode则不那么简单。

一个好的散列函数应该为相等的对象始终产生相等的散列值,并倾向于为不相等的对象产生不相等的散列值,散列冲突是不可避免的,但是好的散列函数应该尽力避免这一点。

《Effective Java》中给出了一种覆盖hashCode的方法:

1、把一个非零常数,比如17,保存在一个int类型的变量result中。

2、对于对象中的每个关键域f,进行以下操作:

  1. 如果该域是boolean,则计算c = f ? 1 : 0。

  2. 如果该域是byte、char、short或者int,则计算c = (int)f。

  3. 如果该域是long,则计算c = (int)(f ^ (f >>> 32))。

  4. 如果该域是float,则计算c = Float.floatToIntBits(f)。

  5. 如果该域是double,则计算c = Double.doubleToLongBits(f)。然后按照long类型计算c = (int)(c ^ (c >>> 32))。

  6. 如果该域是引用类型,则计算c = f.hashCode(),如果该域是null,则c = 0。

  7. 如果该域是一个数组,则把数组的每个元素作为一个单独的域进行处理,JDK1.5中Arrays工具类有一个Arrays.hashCode方法可以完成此操作。

3、result = 31 * result + c。

4、返回result。


一些注意点:

hashCode最关键的一点就是和equals一定要同步,equals相等的对象hashCode一定要一样,所以所谓“关键域”的意思就是equals中会被拿来比较的域,如果equals不比较,那hashCode就不能把该域的计算包含进来。

为什么要用非零的初始值?假设对象的前n个域计算出来的hashCode都是0,那么如果初始值是0的话,对于不同的n会得到相同的结果,增大了hash冲突的可能。

对于8个字节的数据,比如long和double,采用的是高32位和低32位异或的做法,相比直接截取低32位可以减少冲突。HashMap的源码里也有这样的做法。

如果一个类是不可变的,而且计算hashCode的开销较大,则应该把hashCode缓存起来。