听别人说这个hash函数被称为扰动函数,可以减低hash碰撞,我就不信邪了,今天来分析下这个hash函数
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
- 如果
key == null
直接返回0,这也是为啥HashMap只能存储一个null键的原因 - 计算key的哈希值,得到h
- 将h的低16位与高16位进行异或操作
先提前说下,这个hash函数的作用是为了计算key在数组变量table中的位置
/**
* table的长度必须是2的幂,假设table.length是2^7
* 512 = 0000 0000 0000 0000 0000 0000 1000 0000 - 1 =
* 511 = 0000 0000 0000 0000 0000 0000 0111 1111 &
* hash = 0000 0110 0010 0010 1100 0001 0010 0011 =
* index= 0000 0000 0000 0000 0000 0000 0010 0011
* 可以看出hash值保留了低位且index < table.length
* 其实这就是一个取模运算,相当于hash%table.length
* 但是位运算效率高
*/
(table.length - 1) & hash
通过上面索引的算法我们可以得知,索引的值最终值取决于hash的低n位,如果table.length = 1 << 4
,那么n = 4;
table.length = 1 << 5
,那么n = 5;
table.length = 1 << 6
,那么n = 6;
hash函数为啥要进行异或操作?
put的时候需要根据这个hash值去计算索引(在变量table中的位置),但是这个计算过程,只有低位参与了,越少的位数参与计算,2个哈希值低四位碰撞的可能性就越高,所以才有这个异或操作。举个栗子
假设table.length = 1 << 4,以下的分析为了简单都基于这个前提,此时参与异或的部分是高13-16位和最低4位,下面简称高4位和低四位。
key a 的hash值为
1100 0100 0110 0011 1100 0000 1000 0100
key b 的hash值为
0000 1100 0010 0100 1000 1100 0010 0100
虽然a和b的哈希值不一样,但是由于最后只有低四位参与运算,计算出的索引肯定也是一样的,这样的话会导致链表越来越长或者红黑树越来越深。
如果让高位和低位进行异或运算的话,可以减少低四位碰撞的可能性
a ^ (a >>> 16) =
1100 0100 0110 0011 1100 0000 1000 0100
^
0000 0000 0000 0000 1100 0100 0110 0011
=
1100 0100 0110 0011 0000 0100 1110 0111
b ^ (b >>> 16) =
0000 1100 0010 0100 1000 1100 0010 0100
^
0000 0000 0000 0000 0000 1100 0010 0100
=
0000 1100 0010 0100 1000 0000 0000 0000
我看网上大量的文章的说法是:重新计算 hash 的另一个好处是可以增加 hash 的复杂度。当我们覆写 hashCode 方法时,可能会写出分布性不佳的 hashCode 方法,进而导致 hash 的冲突率比较高。通过移位和异或运算,可以让 hash 变得更复杂,进而影响 hash 的分布性。这也就是为什么 HashMap 不直接使用键对象原始 hash 的原因了。
请大家不要相信这种话o(∩_∩)o 哈哈。
很明显,对于2个完全相同hash值,不管怎么移位还是高低位异或,最后的结果肯定还是相同的,难道还能玩出花来?所以移位和异或并不能让hash算法的本质变的复杂,对于完全相同的hash值,移位和异或后该怎么碰撞还是怎么碰撞。。。
在于HashMap来说,我们看一个hash函数的优越性取决于最后计算出来的结果碰撞的可能性高低,碰撞几率越低说明函数越优秀,对于一个计算结果只有低四位才有用的hash函数来说,
不管hash函数设计的多么优秀,原本在32位区间分布均匀的hash值,如果只看低四位的话碰撞的可能性太还是高了(毕竟只有222*2种可能),所以设计复杂的hash函数是没有必要的,所以才用高位参与运算。不过也只是尽量去减少低四位碰撞的几率,但是也不能完全避免碰撞。
我们可以通过数学计算来求出,异或后仍然会发生碰撞的概率。
4位的数总共有2*2*2*2 = 16
种可能,所以a和b的排列组合情况有16*16*16*16=65536
种。
假设有2个hash值a和b,a的低4位记为a1,高四位记为a2,b的低4位记为b1,b的高4位记为b2
我们可以得出以下结论:
1、a和b低4位相同时,我们来计算下碰撞几率:
想要异或的结果相同,那么只能是a1和b2也相同才行,
4位的数总共有2*2*2*2 = 16种可能,
所以
a1 = b1可能的排列组合有:16 * 16 * 16 = 4096
a1 = a2 且 b1 = b2的排列组合有:16 * 16 = 256
所以最后碰撞的几率是:256/4096 = 6.25%
所以我们有93.75的概率不会遇到碰撞,很妥当
代码验证如下:
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
public class Collision {
public static void main(String[] args) throws InterruptedException {
List<int[]> ls = new ArrayList<>();
combination(new int[4], 0, ls);
calculate(ls);
}
/**
* 4位二进制数排列组合
*
* @param bits 4位二进制数
* @param index bits位下标
* @param ls 存储4位二进制数
*/
private static void combination(int bits[], int index, List<int[]> ls) {
for (int i = 0; i < 2; i++) {
bits[index] = i;
if (index == 3) {
ls.add(Arrays.copyOfRange(bits, 0, 4));
} else {
combination(bits, index + 1, ls);
}
}
}
/**
* @param ls
*/
private static void calculate(List<int[]> ls) {
ArrayList<CalculateMeta> calculateMetas = new ArrayList<>();
for (int[] low : ls) {
for (int[] high : ls) {
calculateMetas.add(new CalculateMeta(low, high));
}
}
calCollision(calculateMetas);
}
/**
* 计算碰撞数
*
* @param src 排列组合
*/
private static void calCollision(ArrayList<CalculateMeta> src) {
// a的低位和b的低位相同时碰撞的组合数
int count = 0;
// a的低位和b的低位相同的组合数
int total = 0;
for (CalculateMeta a : src) {
for (CalculateMeta b : src) {
if (a.lowEquals(b)) {
total++;
if (a.xorEquals(b)) {
count++;
}
}
}
}
System.out.println("a的低位和b的低位相同的组合数" + total);
System.out.println("a的低位和b的低位相同时碰撞的组合数:" + count);
}
private static String concat(int[] bits) {
String r = "";
for (int x : bits) {
r += x;
}
return r;
}
private static class CalculateMeta {
private int[] low;
private int[] high;
public CalculateMeta(int[] low, int[] high) {
this.low = low;
this.high = high;
}
public boolean lowEquals(CalculateMeta compare) {
return Objects.deepEquals(low, compare.getLow());
}
public int[] xor() {
int res[] = new int[4];
for (int i = 0; i < 4; i++) {
res[i] = low[i] ^ high[i];
}
return res;
}
public boolean xorEquals(CalculateMeta compare) {
return Objects.deepEquals(xor(), compare.xor());
}
public int[] getLow() {
return low;
}
}
}
结果:
a的低位和b的低位相同的组合数4096
a的低位和b的低位相同时碰撞的组合数:256
2、a和b低4位不同,如果不异或的话,肯定不会碰撞。如果异或的话还是有几率会碰撞的,我们计算下这个概率:
a1 != b1可能的排列组合有:16 * 16 * 15 * 16 = 61440
a1 != b1且a1 ^ a2 = b1 ^ b2的排列组合有:16 * 16 * 15 = 3840
所以最后碰撞的几率是:3840/61440= 6.25%
所以我们有93.75的概率不会遇到碰撞,也很妥当
代码验证如下
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
public class CollisionDiff {
public static void main(String[] args) throws InterruptedException {
List<int[]> ls = new ArrayList<>();
combination(new int[4], 0, ls);
calculate(ls);
}
/**
* 4位二进制数排列组合
* @param bits 4位二进制数
* @param index bits位下标
* @param ls 存储4位二进制数
*/
private static void combination(int bits[], int index, List<int[]> ls) {
for (int i = 0; i < 2; i++) {
bits[index] = i;
if (index == 3) {
ls.add(Arrays.copyOfRange(bits, 0, 4));
} else {
combination(bits, index + 1, ls);
}
}
}
private static void calculate(List<int[]> ls) {
ArrayList<CalculateMeta> calculateMetas = new ArrayList<>();
for (int[] low : ls) {
for (int[] high : ls) {
calculateMetas.add(new CalculateMeta(low, high));
}
}
calCollision(calculateMetas);
}
private static void calCollision(ArrayList<CalculateMeta> src) {
int total = 0;
int count = 0;
for (CalculateMeta a : src) {
for (CalculateMeta b : src) {
if (!a.lowEquals(b)) {
total++;
if (a.xorEquals(b)) {
count++;
}
}
}
}
System.out.println("a的低位和b的低位不相同的组合数:" + total);
System.out.println("a的低位和b的低位不同时碰撞的组合数:" + count);
}
private static String concat(int[] bits) {
String r = "";
for (int x : bits) {
r += x;
}
return r;
}
private static class CalculateMeta {
private int[] low;
private int[] high;
public CalculateMeta(int[] low, int[] high) {
this.low = low;
this.high = high;
}
public boolean lowEquals(CalculateMeta compare) {
return Objects.deepEquals(low, compare.getLow());
}
public int[] xor() {
int res[] = new int[4];
for (int i = 0; i < 4; i++) {
res[i] = low[i] ^ high[i];
}
return res;
}
public boolean xorEquals(CalculateMeta compare) {
return Objects.deepEquals(xor(), compare.xor());
}
public int[] getLow() {
return low;
}
public int[] getHigh() {
return high;
}
}
}
结果如下:
a的低位和b的低位不相同的组合数:61440
a的低位和b的低位不同时碰撞的组合数:3840
第一种情况,a和b低4位相同时异或后产生碰撞的几率也只有256/4096 = 6.25%,如果从所有的排列组合来看,碰撞率 :
(256/4096) * (4096/65536) = 1/256 = 0.390625%,相反我们的收益率:
((4096 - 256)/4096) * (4096/65536) = 15/256 = 5.859375%。
第二种情况,a和b低4位不同时异或后产生碰撞的几率只有3840/61440 = 6.25%(这算是损耗),如果从所有的排列组合来看,碰撞率 :
(3840/61440) * (61440/65536) = 15/256 = 5.859375%。
哈哈,很不幸,如果单从数学的角度来看,总的收益为第一种情况的收益减去第二种情况的损耗:15/256 - 15/256 = 0。悲了个剧~
没错异或和移位操作是很快,但是要说它真的能减少碰撞,就是在蛇皮了。
从统计学的角度来说,这样高低位异或操作,对hash碰撞是零影响的,在不会提高碰撞率的前提下,高低位异或其实是为了解决一些特殊场景下hash散列的结果分布极度不均匀的问题,举个栗子:
public static final int tabLen = 1 << 4;
public static void main(String[] args) {
cal(1298, 32);
}
static void cal(int begin, int total) {
Map<Integer, Integer> sts = new HashMap<>();
Map<Integer, Integer> stsWithXor = new HashMap<>();
for (int i = begin; i < begin + total; i++) {
// 无高低位异或
int index = indexFor(hash(new Float(i)));
Integer repeat = sts.putIfAbsent(index, 1);
if (repeat != null) {
sts.put(index, repeat + 1);
}
// 有高低位异或
int indexXor = indexFor(hashWithXor(new Float(i)));
Integer repeatXor = stsWithXor.putIfAbsent(indexXor, 1);
if (repeatXor != null) {
stsWithXor.put(indexXor, repeatXor + 1);
}
}
Iterator<Map.Entry<Integer, Integer>> it = sts.entrySet().iterator();
while (it.hasNext()) {
Map.Entry<Integer, Integer> next = it.next();
int index = next.getKey();
int repeat = next.getValue();
System.out.println(index + " 出现的次数 " + repeat);
}
System.out.println("------with xor-----------");
it = stsWithXor.entrySet().iterator();
while (it.hasNext()) {
Map.Entry<Integer, Integer> next = it.next();
int index = next.getKey();
int repeat = next.getValue();
System.out.println(index + " 出现的次数 " + repeat);
}
}
static final int hash(Object key) {
int h;
return (key == null) ? 0 : key.hashCode();
}
static final int hashWithXor(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
static final int indexFor(int hash) {
return (tabLen - 1) & hash;
}
结果如下:
0 出现的次数 32
------with xor-----------
2 出现的次数 6
3 出现的次数 8
4 出现的次数 8
5 出现的次数 8
6 出现的次数 2
我们发现在table很小的情况下,连续32个很小的浮点数散列的索引只分布在0上,但是经过异或后分布明显更均匀。
至于连续的很小的浮点数计算后出现这种全0分布,是因为java的float.hashcode()返回的值遵循IEEE754规范:
以二进制表示
- 第31位代表符号位
- 30-23位为指数位
- 22-0位为小数位
对于连续的浮点数,在值不是很大的时候,它的小数部分在进行四舍五入后肯定是0,所以最后散列取模后计算出来的索引肯定也是0