前面 put 方法中说到,需要先把当前key进行哈希处理,我们看下这个方法是怎么实现的。
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }这里,会先判断key是否为空,若为空则返回0。这也说明了hashMap是支持key传 null 的。若非空,则先计算key的hashCode值,赋值给h,然后把h右移16位,并与原来的h进行异或处理。为什么要这样做,这样做有什么好处呢?
我们知道,hashCode()方法继承自父类Object,它返回的是一个 int 类型的数值,可以保证同一个应用单次执行的每次调用,返回结果都是相同的(这个说明可以在hashCode源码上找到),这就保证了hash的确定性。在此基础上,再进行某些固定的运算,肯定结果也是可以确定的。
我随便运行一段程序,把它的 hashCode的二进制打印出来,如下。
public static void main(String[] args) { Object o = new Object(); int hash = o.hashCode(); System.out.println(hash); System.out.println(Integer.toBinaryString(hash)); } //1836019240 //1101101011011110110111000101000然后,进行 (h = key.hashCode()) ^ (h >>> 16) 这一段运算。
//h原来的值 0110 1101 0110 1111 0110 1110 0010 1000 //无符号右移16位,其实相当于把低位16位舍去,只保留高16位 0000 0000 0000 0000 0110 1101 0110 1111 //然后高16位和原 h进行异或运算 0110 1101 0110 1111 0110 1110 0010 1000 ^ 0000 0000 0000 0000 0110 1101 0110 1111 = 0110 1101 0110 1111 0000 0011 0100 0111可以看到,其实相当于,我们把高16位值和当前h的低16位进行了混合,这样可以尽量保留高16位的特征,从而降低哈希碰撞的概率。
思考一下,为什么这样做,就可以降低哈希碰撞的概率呢?先别着急,我们需要结合 i = (n - 1) & hash 这一段运算来理解。
** (n-1) & hash 作用**
//② //这是 put 方法中用来根据hash()值寻找在数组中的下标的逻辑, //n为数组长度, hash为调用 hash()方法混合处理之后的hash值。 i = (n - 1) & hash我们知道,如果给定某个数值,去找它在某个数组中的下标位置时,直接用模运算就可以了(假设数组值从0开始递增)。如,我找 14 在数组长度为16的数组中的下标,即为 14 % 16,等于14 。 18的位置即为 18%16,等于2。
而②中,就是取模运算的位运算形式。以18%16为例
//18的二进制 0001 0010 //16 -1 即 15的二进制 0000 1111 //与运算之后的结果为 0000 0010 // 可以看到,上边的结果转化为十进制就是 2 。 //其实我们会发现一个规律,因为n是2的n次幂,因此它的二进制表现形式肯定是类似于 0001 0000 //这样的形式,只有一个位是1,其他位都是0。而它减 1 之后的形式就是类似于 0000 1111 //这样的形式,高位都是0,低位都是1,因此它和任意值进行与运算,结果值肯定在这个区间内 0000 0000 ~ 0000 1111 //也就是0到15之间,(以n为16为例) //因此,这个运算就可以实现取模运算,而且位运算还有个好处,就是速度比较快。为什么高低位异或运算可以减少哈希碰撞