关于JDK1.7和JDK1.8 hash()方法
前言
HashMap中hash方法返回的值用于哈希散列得到一个下标,JDK1,7和JDK1.8,hash()方法的原理几乎相同,不过还是有不同的地方听我一一道来。
JDK1.7 hash()方法
final int hash(Object k) {
int h = hashSeed;
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
h ^= k.hashCode();
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
static int indexFor(int h, int length) {
return h & (length-1);
}
首先HashMap是一张哈希表,储存的是键值对,在存储时,会计算出键的Hashcode,对该hashcode进行一些变换后,使用该hashcode对HashMap的容量减一取余,计算出下标。
indexFor方法就是计算下标使用的,只不过是采用位运算(&)的方式,这里的位运算其实就是取余(%)的意思
比如
HashMap的容量为16,hash方法的返回值是10101010110
16 - 1 = 15 = 01111
10101010110 & 01111 = 0110 = 6
10101010110 & 01111 这个运算可以理解成
使用位运算的好处
- 从速度上来说,位运算的预算效率要远远快于取余运算
- hashcode的大小是一个4字节的数,也可以理解成一个int类型的数据,也就是说它的取值范围是-2147483648~2147483647,也就说有负数的存在,如果我们不采用位运算,是不是通过取余运算取出一个负的值,对于数组来说负数的下标肯定是不存在的,如果使用位运算就能解决掉负数的问题。大家看一下下面的例子
运行结果:
从运行结果来看,当我们的hashcode为负数进行按位与运算时,得到的结果依然是大于0的
位运算为什么可以代替取模运算
任何数对16的取模的运算结果一定是一个0~15(包括0和15)的数和取出一个数的低4位(即与15按位相与)的结果基本一样。
比如
17 = 10001(B)
15 = 01111(B)
17 & 15 = 10001 & 01111 = 001 = 1(D)
为了么说结果基本一样呢
10 = 1010(B)
7 = 0111(B)
10 & 7 = 1010 & 0111 = 010 = 2(D)
而10 % 7 = 3
我们进行位运算的目的是哈希散列得到下标,并不是得到一个准确的取余结果,这种不一样是可以接受的。
关于hash()返回部分的讲解
h ^= k.hashCode();
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
我相信大家看到这部分代码时,内心时懵逼的,为什么不能直接用键得hashcode呢,非要进行这么多次对hashcode的变化。其实这个一种减少哈希冲突的一种手段。
比如
hashMap的容量为8,(也就是对8 - 1进行按位相与)7的二进制为0111
1101 0101 & 0111 = 101
0101 0101 & 0111 = 101
我们会发现就算hsahcode不一样,但是进行位运算的结果是一样的,因为他们的后三位都相等,这样就会形成哈希冲突
所以
h ^= k.hashCode();
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
目的是对hashcode进行扰乱操作,降低hashcode不同但是位运算结果相同的可能性,降低了哈希冲突
JDK1.8 hash()方法
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
在JDK1.8的实现中,原理上没有太大的变化,可以认为是JDK1.7版本的优化版本,hash方法的返回值通过hashCode()的高16位异或低16位实现的:(h = k.hashCode()) ^ (h >>> 16),主要是从速度、功效、质量来考虑的。以上方法得到的int的hash值,然后再通过h & (table.length -1)来得到该对象在数据中保存的位置。