HashMap哲学中的数学原理,你发现了没有?

tableSizeFor

一般遇到的第一个问题就是tableSizeFor()。

HashMap哲学中的数学原理,你发现了没有?

我来解释一下这部分代码的作用:计算出大于或) 1 i V p等于cap的第一个2的n次幂。 注意:

  • cap参与计算之前要先-1
  • 了解>>>、|的运算规则

他这个算法大概的意思就是先无- b m 1符号右移m位,再将获得的结果与原值进行 | 运算。 比如我们令cap=19,则n=18,二进制表示为10010,无符号右移1( h ~ #位之后是1001。 如果将其对其为( h V / & F n N16位则表示为:0000 0000 0001 0010然后与原值0000 0000 0000 1001与运算结果为:0000 0000 0001 1011后面的几轮我们以此类推即可。 详细的演算过程如下:

HashMap哲学中的数学原理,你发现了没有?

上面的演算结果是11111,翻译为10进制就是31。

return (n < 0)x  ] @ ( V i - & ? 1 : (n >= MAXIMUM_CA1 j O 1 , sPACITY) ? MAXIMUM_CAPACITY : n + 1;

n < MAXIMUM_CAPACITY,所以返回:31 + 1 = 32。 使用 移位 和 或 运算,巧妙的化r + T 5 W 2 R ! %简了问题。注意这里有一个细节, 9 : 0 G ( r就是cap-1,为什么这么做1 & ^ - T呢。假设 x = 2^n,如果我们不处理得到的结果是2 * 2^n,这里就不符合要求。大于等于 x 的第l O K t一个值应该是 x 本身。不信的话,可以动手验证一下。

hash

这里我们不讨论hashCod$ C _ Ue产生的过程,我们只关心后部分。也就是大家所谓的扰动函数。

static finalN m E 4 B w int hash(Object key) {
int h;
return (key == null) ? 0 :
// 我w U 1 /们看最后一行作用
(h = key.hashCode()) ^ (h >>> 16);
}

Jdk对此做出了相应解释。

Computes key.hashCode() and spreads (XORs) higher bits o# ! * L e = [ !f hash

直译就是:计算key.hashCode()并扩展哈希的更高] v # 有必要思考一下:为什么已经得出key的hashCode之后还要大费周章进行 移位 异或的运算呢。假设我们不P 0 q w M进行扰动呢,能有什么影响呢。我们知道HashMap中确定元素所在数组中的位置是通过 & 运算来实现的。

if ((p = tab[i = (n - 1) & hash]7 X H R y) == null)

一个key的hashCode足够大,而当前HashMap的容量不S j [ I | }是足够大的时候,你发现了吗对该key进行定位的重大决定其实只是交给了最后几位。 假设当前容量是16,然后d l j _ 8(n - 1) = 15,其实就是二进制的1111。无论haT S P , @ osh有多少位,不足的我1111只管补足0即可。这时候一个非常尴尬的情况出现了。我管你hashCode多少位只要跟我111 e , W V M ( p v1进行 & 运算,除了最N 7 H ( ~ p ( x后四位前面的一律为0。M ? _ L ( ( R也就是说hash看着位数足够多其实发挥作用的只有最后面的一部分。这样一来,两个has y ] q } Xh只需要低位一致,高位就算多大[ L )差异V L s 8 F他们最后一定定位再同一处。 这显然是源码 5 O 4作者不O M 4愿意看到的结果。其实这里高位参与运算就是为了打乱低位。这样高位不同,低位相同,定位就一定相同的僵局直o F { w ~ X I P接就会被打破。

容量的秘密

我们知道HshMap扩容至原来的2倍,初始容量是16,这么一来ta的容量其实永远都是2^n。在二进制中2^n的数字是极具特点的。转化成二进制之后首位为1,余位为0。那么2^n - 1之后呢,得到的结果更加? Y有规律,一定得到全部为 1 的排列。 请你一定要记得任何数 -1 得到全部是 1 的排列那么这个数字一定是2^+ u & : x = J K /n

final V putVal(int hash, K key, V valut 1 5 V / Be, boolean onlyIfAbsent,  boolean evict)
n = (tab = resize()).length;
(n - 1) & hash
复制代码

在puts R T()方法中想要定位,其实就是获取到Key的5 e / Ghash之后对当前容量求余。但是求余在计算机的世界里比较消耗性能。1 r Z O v S我们可以将hash % n转化为(n - 1) & hash。 & 的运算规则是全部为 1 结果为 1,其余结果为 0。 我们令hasL k s ! U + . O 7h的值为a b c d,(n - 1)的值为m n x y,且上述变量取值范) ) e % w m | 6 O围为[0, 1]。 & 运算结果为t x y z,最完美的情况是: 四位数值每一位都可能是 0 或者 1,那么得到的排列有2 * 2 *Y D 3 : - 7 & q 2 * 2 = 16 种。我们继续假设如果m n x y其中有一个数字y V S k d b等于 0 呢?结果的排列就只剩U O j下2 * 2 * 2 = 8种。出现两位为 0 的话排列就只剩下2 * 2 = 4种。 这么理论不免有些枯燥。因为我们是明确知道当前7 C 0 d t _ 4容量的! 7 6 o z ,。如果 n = 1110,n -? * = B z } R * 1 = 1101,相当于你数组长度为 14 时,只会出现 8 种排列,那么对应成HashMap的定位问题,就是6 R b x x w s说数组长度是 14 时,足足有 6 个位置是永远浪费的。如果存在这种大量浪费的情况,势必会导致HashMap频频扩容,损耗性能。 那怎么解决呢?怎么让所有的位置都有可能被定位,对应成上述数学模型解决方案其实就是(n - 1)所有$ M M位必须全为 1。 那如果(n - 1)所有位必须全为 1。则 n = 2^x 成立。

扩容的秘密

if ((& | We.hash & oldCap) == 0)
newTab[j] = loHead;
newTaF w b O 0 l Rb[j + oldCap] = hiHead;

这里我当时看的时候I Y r U B x $ p百思不得其解,直到我发现这里(e.hash &aG . q * K v 7mp; oldCap)不是(e.O ` e K 0 x &hash & (oldCap - 1))这两者天壤: { K f w : 3 M之别。这句代码这里的意思就是其实就是判断oldCap最` 4 { c Q Z高位对应e.hash相对应位置上的值是否为 1。 这个很重要因为如; U s &果对应的位置为 1,直接表示ta需要搬家,而搬到哪里去呢?数组原来位置对应的数字加上原容量即可。如果是 0,那就待在原地即可。其实这里只要二进制和数学学的好的,应该是瞬间就可以反应的过来。 扩容之后(e.hash & (newCap - 1))其实只有newCap的最高位对应的e.ha+ p &sh的那一位可以B 6 4 U # e $ p &发挥作用,newCap最高位前面的全是0不影响结果,后面F u Y i k ^ m呢跟(e.hash & (oldCap - 1))的结果又一摸一样也不影响结果。还不明白,那我就偷一张图:

HashMap哲学中的数学原理,你发现了没有?

要是还不明白咱就代数演示一下:
假设原容量n=10000,n - 1 = 1111
假设key.hash = 10001
那么ta所在的位置是 1
然后扩容一下
现在n=100000,n - 1 = 11111
那么ta所在的位置是10001

作者:颜如玉
原文链接:https://juejin.im/post/5e5251fc6fb9a07c7d005b7c

上一篇

商务部:全国大型超市电商快递业等开业率超过90%

下一篇

推动金融业变革,智能金融正当时

评论已经被关闭。

插入图片
返回顶部