java8 HashMap之hash函数

听别人说这个hash函数被称为扰动函数,可以减低hash碰撞,我就不信邪了,今天来分析下这个hash函数

    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    } 
  1. 如果key == null直接返回0,这也是为啥HashMap只能存储一个null键的原因
  2. 计算key的哈希值,得到h
  3. 将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

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值