"重写equals方法时为啥要重写hashCode方法?"这个可能是面试出场率最高的问题了, 没有之一. 不重写hashCode方法会导致所有使用hash值的集合类处理异常, 比如HashMap和HashSet. 原理很好理解, 以HashMap为例, 在get和put时, 会认为equals和hash值都相等的元素才是同一个元素.
Object中提供的hashCode方法是一个native方法, 它的底层实现逻辑是将对象内存地址转换为整数返回, 所以可以保证同一个对象的hashCode返回是一致的.
下面java规范中关于hashCode的部分
- 一致性, 对于一个对象, 当equals方法使用的比较属性没有变化时, hashCode多次执行结果是一样的.
- 如果两个对象的equals方法判断相等, 那么这两个对象的hashCode方法返回值也必须相等.
- equals判断不相等的两个对象, 不要求这两个对象的hashCode返回结果一定不一样, 但是如果不一样的话会提供系统性能.
作者给出了实现hashCode的方法
- 定义一个整数result = 17.
- 将每一个会影响equals方法的属性转换为一个整数.
- 将每一个属性的整数结果通过下面的公式累加到result
result = result * 31 + c // c是属性的整数值.
在第二步中作者给出了每一个类型转换为整数的方式, 如下(f表示属性):
- 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类型处理.
- 引用类型: 使用其hashCode方法. 对于null返回0.
- 数组类型: 对其中每一个元素按照上述规则求值, 或者使用Arrays.hashCode().
关于上面的规则, 有两个问题
- 这么多转换规则, 我不可能记住.
- 上面17, 31为啥是17, 31?
问题一:
刚看到这里的时候就懵了, 这么多都要记? 看到第四条Float.floatToIntBits(f)想到一点, 不如看看它们的包装类型是怎么做的. 果然, 这些数字类型的包装类型会实现一个静态的hashCode方法, 入参是实际值, 实现内容跟作者说的基本一样(jdk 1.8), 除了boolean, 作者说的是1和0, jdk中是1231和1237. 所以对于数字类型, 我们不需要再记忆这些, 直接使用对应的静态方法即可. 比如double类型, 我们可以直接返回Double.hashCode(f), Double的hashCode如下:
@Override
public int hashCode() {
return Double.hashCode(value);
}
public static int hashCode(double value) {
long bits = doubleToLongBits(value);
return (int)(bits ^ (bits >>> 32));
}
问题二:
回忆下文章开始的hashCode规范, 三条规则概括起来是两条内容: 一是hashCode返回要正确, 不会影响业务; 二是好的hashCode会系统提高性能.
问题二中的17和31就要从系统性能说起. 那么hashCode的值为什么会影响系统性能? 举个例子, 如果hashCode方法对于任何对象都返回1会怎么样? 首先这样是满足规范要求, 不影响业务判断. 但是当我们把这样的对象插入到hash表中时会发现, 因为所有的对象的hash值一样, 所以都插入到了同一个槽中, 也就是说hash表退化成了链表结构, 查询性能从O(1)恶化为O(n).
为了让hash表更平均需要hashCode返回值更合理, “更合理"意思就是"更少的hash冲突”, 所以hashCode方法多使用17和31这样的质数来参加运算, Boolean的hashCode方法中的1231和1237也是同样道理.
那为什么质数的hash冲突更少? 这个应该有严格的数学推论, 简单来说, hash槽是通过 hashCode % n (n为hash表大小) 确定的, 如果值为0表示可以被n整除, 那么所有可以被n整除的hashCode都会被分配到同一个槽中, 所以如果是质数的话就不会出现整除情况(HashMap大小是2的n次方), 所以冲突的可能性更小(以上没有数学证明, 只是感觉~).