Foo说Bar道——HashMap篇之hash()

HashMap是广大程序员在做项目的时候,最喜欢使用的一个工具,用的最多但了解的并不与使用频率成正比,很多人都是只停留在会put/get而已。这样在面试的时候其实很不吃香的,知道面试会问HashMap就去网上看面经,但面经上的关于HashMap的东西嘛。。。千篇一律,看完之后食之无味弃之可惜。所以去看源码是一个了解HashMap很好的方式也是很重要的途径。这篇文章不会涉及太多HashMap源码的东西,我主要拿来其中的一点与大家进行分享——散列方法static final int hash(Object key);

大家应该都知道在java8中hash方法源码张这个样子:

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

看上去,没啥太复杂的东西,就是拿来k的hashCode,高低16位做亦或,在后续进行模拟取模操作。

那么问题来了,你真的知道他为什么要高低16位亦或吗?或许你会跟我说,人家网上不是都说了,为了让hash做的更均匀,尽量避免冲突吗?那我们就来做一个实验,看一看这个东西到底是不是像网上说的那样:

实验步骤很简单,我们随便敲一些字符串:

List(
    "asf","132sg","23ga","13erasd","alireio",
    "2oosoi","qooizoi","!#@$sdgta","ASD123zadsf#@","qasdolkado!#@",
    "}{K[PKpoa","ptqoewto","!sdyq43`","asdotjq435DSGj","qwl4tkjslgja_welkrj@",
    "lotjqoi4#@%","1234"
)

这里面超过16个元素,所以让hashMap的长度设为32,也就是最后要取hash的后5位,然后去分析两种情况,结果如下:

0000 0000 0000 0001 0111 1010 0111 0100 <- asf
0000 0010 1100 1010 0111 1000 1010 0100 <- 132sg
0000 0000 0001 0111 1000 0110 1101 1011 <- 23ga
0111 1100 1101 1101 0001 0001 1100 0011 <- 13erasd
1100 1001 1000 0100 1101 0101 0011 0111 <- alireio
0101 1011 1010 0010 1011 0010 1011 1011 <- 2oosoi
0001 1101 0101 0111 1101 1000 0101 1100 <- qooizoi
1100 1110 0001 1100 0100 1101 0101 1101 <- !#@$sdgta
1110 0100 1101 1010 0100 1111 0000 1101 <- ASD123zadsf#@
0110 0101 1011 0010 0001 0000 1001 1111 <- qasdolkado!#@
0110 1101 1110 0010 0100 0001 1001 1001 <- }{K[PKpoa
0011 0001 1111 0110 0000 0000 1100 1111 <- ptqoewto
1101 1000 1111 0101 0100 1000 1011 0111 <- !sdyq43`
0101 0011 0100 1011 1010 1011 0100 1010 <- asdotjq435DSGj
1110 1001 1000 1110 0110 1101 1011 1100 <- qwl4tkjslgja_welkrj@
0000 1011 0111 1100 0011 1101 1100 0110 <- lotjqoi4#@%
0000 0000 0001 0111 0000 1000 0100 0010 <- 1234
========================直接取后五位==========================
00010
00011
00100
00110
01010
01101
01111
10100
*10111
*10111
11001
*11011
*11011
*11100
*11100
11101
11111
========================高低十六位亦或==========================
*00001
*00001
00010
01011
01100
01101
01110
10010
10011
*10101
*10101
10111
*11001
*11001
11010
11011
11110

观察上面的结果我们发现,两种情况都有三组冲突,无论怎么取字符串其实冲突的相差都不多,(后面的证明也说明了网上的大部分都是口口相传错的结论,经不起推敲的,不过现在的中国普遍不都这样吗,,,点到为止,,,)

当时在我研究hash()函数的时候,觉得这个现象比较诡异,因为一直以来我也是觉得这个肯定就是降低了冲突率啊!所以,为了满足我的好奇心,经过了一些推倒,最终得出的结论是:对于任意拿来两个整数a1,a2,无论进行怎样的变换,二进制a与b的后n位冲突的概率都是等于 2^{-n} ,另一方面取出m个a,后n位冲突的概率是 1-2^n!/((2^n-m)! * 2^{n*(m-1)})

就是说网上说的什么hash函数为了解决hashCode后n位相等的情况,到哪里查都是这个答案,那么我想问你们想过没有,一个整数位操作后得到的,难道不也是一个整数吗?难道他就不会出现相等的情况吗?所以对这类人我不做过多评价。

所以这么看来hash函数其实根本没有降低冲突率,冲突率降低的唯一方法是降低 m/n 的值,熟悉吗?这不就是HashMap的额定大小和负载因子干的活吗?

那么问题来了,那这个hash存在的意义又是什么呢???我思考了许久,发现了一个事情,就是说上面的推论的大前提是:任意整数!也就是说前提是说key的hashCode足够均匀!那么这个时候,我们是否应该把关注点放在hashCode身上了?如果hashCode足够均匀,那么hash函数自然就起不到作用,那如果他不是足够均匀的呢?

根据这个入手点,我最后得出一个结论,

这个高低十六位亦或的操作,是基于假设key类的设计者在设计hashCode的时候可能设计出bad hashCode,在设计这个bad hashCode的时候bad的点可能在于hashCode的后n位不均匀,而不是bad在hashCode的高低16位亦或后的后n位不均匀!这就是跳了一层的感觉。

可能比较绕口,我们来举个栗子说明一下。

对于一个类,我们如果想重写hashCode,一般会根据某个字段某个属性,按照一定的公式,去计算吧?那么这个计算的出的结果,很可能遵循着某个线性的规律,这个规律导致了对象的hashCode的后n为可能比较集中。

那么,对于高低16位亦或来说就不一样了,我总不能特意去设计一个hashCode经过高低16位亦或的结果比较集中的hash吧?比如?像下面这种?

int hashCode(){
    String bin = Integer.toBinaryString( f(attr1, attr2, attr3 ));
    // g : "xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx" => "xxxx xxxx xxxx 1010 xxxx xxxx xxxx 0101"
    return g(bin); 
}

那我估计这样的类作为key的话,HashMap的设计者可能会跑出来杀了你的

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值