HashMap中的hash函数

在写一个HashSet时候有个需求,是判断HashSet中是否已经存在对象,存在则取出,不存在则add添加。HashSet也是经过HashMap实现,只用了HashMap的key,value都存储一个赘余的Object,以下是HashSet中持有的HashMap对象,add函数:数组

  private transient HashMap<E,Object> map;

    // Dummy value to associate with an Object in the backing Map
    private static final Object PRESENT = new Object();
 
public boolean add(E e) {
    return map.put(e, PRESENT)==null;
}

中在HashMap中的hash函数判断key是否存在,以下图所示:网络

static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

在看到这段代码时疑问产生了,为何hash函数这么设计?查过资料以后解释以下(以下内容来自网络-知乎胖胖的答案):函数

这段代码叫“扰动函数”spa

你们都知道上面代码里的key.hashCode()函数调用的是key键值类型自带的哈希函数,返回int型散列值。

理论上散列值是一个int型,若是直接拿散列值做为下标访问HashMap主数组的话,考虑到2进制32位带符号的int表值范围从-21474836482147483648。先后加起来大概40亿的映射空间。只要哈希函数映射得比较均匀松散,通常应用是很难出现碰撞的。

但问题是一个40亿长度的数组,内存是放不下的。你想,HashMap扩容以前的数组初始大小才16。因此这个散列值是不能直接拿来用的。用以前还要先作对数组的长度取模运算,获得的余数才能用来访问数组下标。源码中模运算是在这个indexFor( )函数里完成的。设计

bucketIndex = indexFor(hash, table.length);

indexFor的代码也很简单,就是把散列值和数组长度作一个"与"操做,code

static int indexFor(int h, int length) {
        return h & (length-1);
}

顺便说一下,这也正好解释了为何HashMap的数组长度要取2的整数幂。由于这样(数组长度-1)正好至关于一个“低位掩码”。“与”操做的结果就是散列值的高位所有归零,只保留低位值,用来作数组下标访问。以初始长度16为例,16-1=15。2进制表示是00000000 00000000 00001111。和某散列值作“与”操做以下,结果就是截取了最低的四位值。对象

    10100101 11000100 00100101
&   00000000 00000000 00001111
----------------------------------
    00000000 00000000 00000101    //高位所有归零,只保留末四位

但这时候问题就来了,这样就算个人散列值分布再松散,要是只取最后几位的话,碰撞也会很严重。更要命的是若是散列自己作得很差,分布上成等差数列的漏洞,刚好使最后几个低位呈现规律性重复,就无比蛋疼。blog

时候“扰动函数”的价值就体现出来了,说到这里你们应该猜出来了。看下面这个图,内存

右位移16位,正好是32bit的一半,本身的高半区和低半区作异或,就是为了混合原始哈希码的高位和低位,以此来加大低位的随机性。并且混合后的低位掺杂了高位的部分特征,这样高位的信息也被变相保留下来。

最后咱们来看一下Peter Lawley的一篇专栏文章《An introduction to optimising a hashing strategy》里的的一个实验:他随机选取了352个字符串,在他们散列值彻底没有冲突的前提下,对它们作低位掩码,取数组下标。ci

结果显示,当HashMap数组长度为512的时候,也就是用掩码取低9位的时候,在没有扰动函数的状况下,发生了103次碰撞,接近30%。而在使用了扰动函数以后只有92次碰撞。碰撞减小了将近10%。看来扰动函数确实仍是有功效的。但明显Java 8以为扰动作一次就够了,作4次的话,多了可能边际效用也不大,所谓为了效率考虑就改为一次了。

相关文章
相关标签/搜索