hashCode需满足的条件
- 当equals方法中涉及的参数没有改变时,hashCode应保持不变
- 如果根据equals方法,两个对象是相等的,那么这两个对象的hashCode应该一样
- 两个对象如果不相等,hashCode不强制要求不一样,但是如果能保证不一样,对哈希的效率会比较有帮助
最重要的是第二点,相等的对象必须有相同的hashCode,由于默认的hashCode方法针对每一个对象返回一个固定的随机值(有的实现是根据对象地址返回值,相当于每一个对象对应一个固定的随机值),所以当我们使用equals方法的同时,必须override(重写)hashCode方法,以满足这一点。
1 | public class TantanitReaderPhone { |
上面的代码是一个手机号码的例子,手机号码由区号(比如中国是86)和本国手机号构成。我们重写了equals方法,但hashCode使用的仍然是父类也就是Object类的方法,可以理解为是一个随机数。在main函数中,我们定义了一个以TantanitReaderPhone为key的hashMap,保存并试图取出一个value值。需要注意的是,我们保存和取出时,使用的是两个不同的对象(两次都是new一个新的对象),但两个对象有着相同的areaCode和localNumber,根据我们重写的equals方法,这两个对象是相等的。但是由于我们没有重写hashCode方法,这两个对象的哈希值不同,所以使用第二个对象无法在hashMap里找到第一次存进去的值。
这是因为,哈希表的每个分区,只会对应有限的哈希值,并存储这些哈希值对应的对象。所以哈希值不同,首先找不到对应的分区,即使碰巧哈希表有分区同时对应着两个哈希值,由于哈希表往往会进行优化,对哈希值先进行判断,所以不相等的哈希值找不到对应的对象。
所以,当需要根据哈希值进行存储时,应该重写hashCode方法,根据字段值生成对应的hashCode(哈希值)。下面讲解如何计算hash值,并且改写我们上面的例子,重写hashCode方法,再执行以下main函数,看看有什么结果。
如何计算hashCode
- 生成一个int类型的变量result,并且初始化一个值,比如17
- 对类中每一个重要字段,也就是影响对象的值的字段,也就是equals方法里有比较的字段,进行以下操作:
a. 计算这个字段的值filedHashValue
b. 执行 result = 31 * result + filedHashValue; 更新结果
而要如和计算这个字段的值filedHashValue值呢,根据字段类型分为三种情况,一种是基础数据(比如int,boolean,),一种是对象,还有一种是数组。
如何计算每个重要字段的值
如果字段是基础数据,假设数值为f,根据类型
- boolean,true返回1,false返回0
- byte, char, short, int返回对应的int值
- long,返回f^(f>>>32)
- float,返回Float.floatToIntBits(f)
- double,执行Double.doubleToLongBits(f)转为long,再返回compute(int)(f^(f>>>32))
而如果字段是对象,如果是null,返回0,否则根据上述【如何计算hashCode】中所述步骤1和2,计算这个对象的hashCode,作为这个字段的值。实际上是一个递归的过程。
而如果字段是数组,则对每一个元素进行计算,得到filedHashValue,并执行result = 31 * result + filedHashValue;
这里补充一下HashMap对象是如何计算哈希值的,JDK中,HashMap类自身已经实现了hashCode方法。一个hashMap其实是entry的数组。
AbstractMap中,hashCode对数组中的每一个entry计算哈希值,并得到所有哈希值的和。
1 | public int hashCode() { |
而每一个entry按照以下方式计算哈希值(JDK1.7之后的版本)。
1 | public final int hashCode() { |
这个实现非常巧妙,由于entry由key和value生成,所以将两个哈希值求与,既保证具有相同key和value的entry一定具有相同的哈希值,又保证了效率(与操作的效率很高)。
计算hashCode的注意事项
- 不能包含equals方法中没有的字段,否则会导致相等的对象可能会有不同的哈希值
- 和equals方法一样,不应当包含可以通过其他字段计算出来的字段
- 不要尝试减少计算重要字段,虽然这样做,在算hash值时会比较快,但会导致同一个hash值对应过多对象,比如TantanitReaderPhone中如果只使用areaCode字段计算哈希值,那么所有来自中国的手机号都会是同一个hash值,显然哈希表的性能会很低。
- 如果对象的值不可更改,应当考虑延迟计算并且缓存哈希值,比如JDK中String的值是不可变的(每一次更改值都会生成新的String对象),所以对hash值的计算使用了延迟计算,并且缓存,String的hashCode方法如下:
1 | public int hashCode() { |
TantanitReaderPhone的哈希值计算
1 |
|
由于areaCode和localNumber都是用来区分TantanitReaderPhone的重要字段,所以根据这两个字段来计算哈希值。这两个字段都是String类型,直接调用String自带的hashCode方法(areaCode和localNumber假定都不为null)。
将TantanitReaderPhone的hashCode方法按照上述代码进行重写后,再执行main函数,打印出“张三”,成功从hashMap中取出对应的值了。您可以复制一下TantanitReaderPhone的代码,亲自试试。