String.class的hashCode方法如下:
public int hashCode() {
int h = hash;
if (h == 0 && value.lengt > 0) {
char[] val = value;
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i]
}
hash = h
}
return h;
}
上面方法中有一个写死固定值31,想必大家在看String的hashCode方法源码时都会有这个疑问,为什么是31?
Java8中HashMap获取hash值的方法为:
static final int hash() {
int h;
return (key == null) ? 0 : (h = key.hashCode() ^ (h >>> 16));
}
get方法中获取key的下标值方法为:n是map的容量大小
(n - 1) & hash
看到HashMap的获取hash方法的源码时,都会思考为什么使用扰动函数计算,为什么不能直接用key的hashCode的值?
hashCode() 00000000 11111111 00000000 00001010 hash值
hashCode() >>> 16 00000000 00000000 00000000 11111111 右移16位
hashCode() ^ (hashCode() >>> 16) 00000000 11111111 00000000 11110101 异或高低位(相同为0,不相同为1)
hashCode() ^ (hashCode() >>> 16) & 15 00000000 00000000 00000000 00001111 与运算下标(都为1则为1,否则为0)
00000000 00000000 00000000 00000101
= 0101
= 5
HashMap的初始化含量是16,最大容量是2的30次方,而且容量大小必须是2的倍数,因为只有2的倍数在减1的时候,二进制都是1,从而在与hash值做与运算的时候随机性更大。
/**
* The default initial capacity - MUST be a power of two.
*/
static final int DEFAULT_INITAL_CAPCITY = 1 << 4
如果我们传的值不是2的倍数,比如我们传17,这个时候HashMap会怎么处理呢?
HashMap构造方法里面,会调用一个tableSizeFor方法进行计算,得到大于我们传的值最小的2倍数的数。
public HashMap(int initialCapacity, float loadFactor) {
...
this.loadFactor = loadlFactor;
this.threshold = tablSizeFor(initialCapacity);
}
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUN_CAPACITY ? MAXIMUM_CAPACITY : n + 1);
}
MAXIMUN_CAPACITY = 1 << 30,这个是临界范围,也就是最大的Map集合。
tableSizeFor都是在向右移1、2、4、8、16位,然后做或运算(两个位都为0则为0,否则为1),因为这样子可把二进制的各个位置都填上1,加上1之后,就是一个标准的2的倍数的数了。
可以把传入17初始化计算阙值的过程用图展示出来,方便理解,最后得到的数就是32
17 - 1 10000
n >>> 1 01000
n | n >>> 1 11000
n >>> 2 00110
n | n >>> 2 11110
n >>> 4 00001
n | n >>> 4 11111
n >>> 8 00000
n | n >>> 8 11111
n >>> 16 00000
n | n >>> 16 11111 = 31
static final float DEFAULT_LOAD_FACTOR = 0.75f;
负载因子用于当容量超过某个阙值时,要进行扩容操作。在HashMap中,负载因子决定了数据量多少了以后可以进行扩容。比如HashMap的容量大小为16,当容量超过 16*0.75=12个元素时,就要进行扩容操作了,这样子做的原因是因为可能出现即使你的元素数量比容量大时也不一定能填满容量,因为某些位置会出现碰撞,使用链表存放了,如果存在大量的链表,这样子就失去了Map的数组的性能了。所以要选择一个合理的大小进行扩容,HashMap默认值是0.75,当阙值容量占了3/4时进行扩容操作,减少Hash碰撞。同时0.75是一个默认构造值,在创建HashMap也可以做调整,比如你希望用更多的空间换取时间,可以把负载因子调的更小一点,减少碰撞。
HashMap在扩容的时候要把原先的元素拆分到新的数组中,拆分过程中,原jdk1.7中会需要重新计算哈希值,但在jdk1.8中进行了优化,不再重新计算,提升了拆分的性能。
String key = "zuio";
int hash = key.hashCode() ^ (key.hashCode() >>> 16);
System.out.println("zuio的扰动hash值:" + Integer.toBinaryString(hash));
System.out.println("容量为16的下标二进制值:" + Integer.toBinaryString(hash & (16 - 1) ));
System.out.println("容量为16的下标十进制值:" + ((16 - 1) & hash));
System.out.println("zuio hash值原容量16与运算结果为:" + (hash & 16));
System.out.println("容量为32的下标二进制值:" + Integer.toBinaryString(hash & (32 - 1) ));
System.out.println("容量为32的下标十进制值:" + ((32 - 1) & hash));
String key2 = "plop";
int hash2 = key2.hashCode() ^ (key2.hashCode() >>> 16);
System.out.println("zuio的扰动hash值:" + Integer.toBinaryString(hash2));
System.out.println("容量为16的下标二进制值:" + Integer.toBinaryString(hash2 & (16 - 1) ));
System.out.println("容量为16的下标十进制值:" + ((16 - 1) & hash2));
System.out.println("plop hash值与原容量16与运算结果为:" + (hash2 & 16));
System.out.println("容量为32的下标二进制值:" + Integer.toBinaryString(hash2 & (32 - 1) ));
System.out.println("容量为32的下标十进制值:" + ((32 - 1) & hash2));
// 上面输出结果为
// zuio的扰动hash值:1110010011100110011000
// 容量为16的下标二进制值:1000
// 容量为16的下标十进制值:8
// zuio hash值与原容量16与运算结果为:16
// 容量为32的下标二进制值:11000
// 容量为32的下标十进制值:24
// plop的扰动hash值:1101001000110011101001
// 容量为16的下标二进制值:1001
// 容量为16的下标十进制值:9
// plop hash值与原容量16与运算结果为:0
// 容量为32的下标二进制值:1001
// 容量为32的下标十进制值:9
通过上面两个例子可以得出以下结论:原hash值与原容量进行&运算,如果结果为0,则下标位置不变,如果不为0,则新的下标在原先的位置上加上原先的容量(我只举了两个例,可以多举些例子,最后都可以得到上面结论)。在HashMap扩容方法resize核心代码如下:
final Node<K, V>[] resize() {
...
// hash值与原容量&运算
if ((e.hash & oldCap) == 0) {
// 原索引
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
// 原索引 + 原容量
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
}
HashMap每一次执行扩容后,数组长度都变成原来的2倍,所以就是数组长度转为二进制后比原来多了一位,比如原先16-1,二进制为1111,扩容之后为32-1,二进制为11111,二进制都多了一位。多出来的这一位与hash值的同一位做&运算,结果为0则索引不变,结果为1则索引为原索引+原数组长度,栗子如下:
多出来的二进制一位1与hash做与运算为1, 为索引为原索引 + 原数组长度
原数组上次16-1的二进制: 0000 0000 0000 0000 0000 0000 0000 1111
新数组长度32-1的二进制: 0000 0000 0000 0000 0000 0000 0001 1111
字符串zuio的扰动hash值: 0000 0000 0011 1001 0011 1001 1001 1000
多出来的这一位与运算结果 1
多出来的二进制一位1与hash做与运算为0, 为索引为原索引
原数组上次16-1的二进制: 0000 0000 0000 0000 0000 0000 0000 1111
新数组长度32-1的二进制: 0000 0000 0000 0000 0000 0000 0001 1111
字符串plop的扰动hash值: 0000 0000 0011 0100 1000 1100 1110 1001
多出来的这一位与运算结果 0