参考:https://www.cnblogs.com/zxporz/p/11204233.html
数学知识回顾
<< : 左移运算符,num << 1,相当于num乘以2 低位补0
举例:3 << 2
将数字3左移2位,将3转换为二进制数字0000 0000 0000 0000 0000 0000 0000 0011,然后把该数字高位(左侧)的两个零移出,其他的数字都朝左平移2位,最后在低位(右侧)的两个空位补零。则得到的最终结果是0000 0000 0000 0000 0000 0000 0000 1100,则转换为十进制是12。
数学意义:
在数字没有溢出的前提下,对于正数和负数,左移一位都相当于乘以2的1次方,左移n位就相当于乘以2的n次方。
>>: 右移运算符
举例:11 >> 2
则是将数字11右移2位,11 的二进制形式为:0000 0000 0000 0000 0000 0000 0000 1011,然后把低位的最后两个数字移出,因为该数字是正数,所以在高位补零。则得到的最终结果是0000 0000 0000 0000 0000 0000 0000 0010。转换为十进制是3。
数学意义:
右移一位相当于除2,右移n位相当于除以2的n次方。这里是取商哈,余数就不要了。
>>> : 无符号右移,忽略符号位,空位都以0补齐
按二进制形式把所有的数字向右移动对应位数,低位移出(舍弃),高位的空位补零。对于正数来说和带符号右移相同,对于负数来说不同。 其他结构和>>相似。
% : 模运算 取余
简单的求余运算
^ : 位异或 第一个操作数的的第n位于第二个操作数的第n位相反,那么结果的第n为也为1,否则为0
0^0=0, 1^0=1, 0^1=1, 1^1=0
& : 与运算 第一个操作数的的第n位于第二个操作数的第n位如果都是1,那么结果的第n为也为1,否则为0
0&0=0, 0&1=0, 1&0=0, 1&1=1 eg:用if ((a & 1) == 0) 代替 if (a % 2 == 0)来判断a是不是偶数。
| : 或运算 第一个操作数的的第n位于第二个操作数的第n位 只要有一个是1,那么结果的第n为也为1,否则为0
0|0=0, 0|1=1, 1|0=1, 1|1=1
~ : 非运算 操作数的第n位为1,那么结果的第n位为0,反之,也就是取反运算(一元操作符:只操作一个数)
~1=0, ~0=1
下面是获取put方法中,get方法中获取key的hash值的方法
//重新计算哈希值
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);//key如果是null 新hashcode是0 否则 计算新的hashcode
}
根据上面的说明我们做一个简单演练
将h无符号右移16为相当于将高区16位移动到了低区的16位,再与原hashcode做异或运算,可以将高低位二进制特征混合起来
从上文可知高区的16位与原hashcode相比没有发生变化,低区的16位发生了变化
我们可知通过上面(h = key.hashCode()) ^ (h >>> 16)进行运算可以把高区与低区的二进制特征混合到低区,那么为什么要这么做呢?
我们都知道重新计算出的新哈希值在后面将会参与hashmap中数组槽位的计算,计算公式:(n - 1) & hash,假如这时数组槽位有16个,则槽位计算如下:
仔细观察上文不难发现,高区的16位很有可能会被数组槽位数的二进制码锁屏蔽,如果我们不做刚才移位异或运算,那么在计算槽位时将丢失高区特征
也许你可能会说,即使丢失了高区特征不同hashcode也可以计算出不同的槽位来,但是细想当两个哈希码很接近时,那么这高区的一点点差异就可能导致一次哈希碰撞,所以这也是将性能做到极致的一种体现
异或运算能更好的保留各部分的特征,如果采用&运算计算出来的值会向0靠拢,采用|运算计算出来的值会向1靠拢
1、为了让哈希后的结果更加均匀
这个原因我们继续用上面的例子来说明
假如槽位数不是16,而是17,则槽位计算公式变成:(17 - 1) & hash
从上文可以看出,计算结果将会大大趋同,hashcode参加&运算后被更多位的0屏蔽,计算结果只剩下两种0和16,这对于hashmap来说是一种灾难
上面提到的所有问题,最终目的还是为了让哈希后的结果更均匀的分部,减少哈希碰撞,提升hashmap的运行效率
自己的理解:
1. HashMap采用的是懒加载,在创建好map的时候,并没有初始化 Node
2. tab在默认情况下是16,为什么是16,请看源码
//这个是计算HashMap中key的hashCode值 ,至于为什么还要讲h逻辑右移16位,上面我抄的作业已经将的很清楚了,为了 让key的后16位更散列,使其具备前16位和后16的特征,为后面讲key放在哪个桶上面做准备 static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node
[] tab; Node p; int n, i; if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; if ((p = tab[i = (n - 1) & hash]) == null) //重点是这一行 讲key放在哪一个桶上面 tab[i] = newNode(hash, key, value, null); else { Node e; K k;
有人怀疑两种运算效率差别到底有多少,我做个测试:
/**
*
* 直接【求余】和【按位】运算的差别验证
*/
public static void main(String[] args) {
long currentTimeMillis = System.currentTimeMillis();
int a=0;
int times = 10000*10000;
for (long i = 0; i < times; i++) {
a=9999%1024;
}
long currentTimeMillis2 = System.currentTimeMillis();
int b=0;
for (long i = 0; i < times; i++) {
b=9999&(1024-1);
}
long currentTimeMillis3 = System.currentTimeMillis();
System.out.println(a+","+b);
System.out.println("%: "+(currentTimeMillis2-currentTimeMillis));
System.out.println("&: "+(currentTimeMillis3-currentTimeMillis2));
}
结果:783,783
%: 359
&: 93
————————————————
版权声明:本文为CSDN博主「四滴火」的原创文章,遵循CC 4.0 by-sa版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/sidihuo/article/details/78489820
首先 这个长度应该是2^n,因为(2^n -1) 的二进制有效数字都是1,那么为什么不能用0呢,因为后面的&运算的时候0-0的0,0-1的0,这样结果只会是0,会发生很多的碰撞的概率,要想结果更加的散列,只有保证 & 运算的时候,有一个对象的有效数字全是1.
举个栗子:
- (2-1)>>>>>0001
- (4-1)>>>>>0011
- (8-1)>>>>>0111
- (16-1)>>>>1111
另外,额外记录一点,如果你想取234对8的余数是多少,就可以使用 234&(8-1),余数为2,这个前提a想对b取余时,b必须是2^n才能这样玩,还是上面讲过的2^n -1的二级制 会全是1,这样,取余的时候,结果就是高位全是0,低位就可以保留下来,保留下来的低位就是余数了
栗子:
234想对8取余数,就是 234&(8-1)=?
234 11101010
8-1=7 00000111
得 2 00000010
// preserve order Node
loHead = null, loTail = null; Node hiHead = null, hiTail = null; Node next; do { next = e.next; if ((e.hash & oldCap) == 0) { //扩容的时候,还是原来的角标 if (loTail == null) loHead = e; else loTail.next = e; loTail = e; } else { //扩容后的hash桶索引要 + 16 if (hiTail == null) hiHead = e; else hiTail.next = e; hiTail = e; } } while ((e = next) != null); if (loTail != null) {// 骚操作 loTail.next = null; newTab[j] = loHead; } if (hiTail != null) { // 骚操作 hiTail.next = null; newTab[j + oldCap] = hiHead; } }
元素在重新计算hash之后,因为n变为2倍,那么n-1的mask范围在高位多1bit,因此,我们在扩充HashMap的时候,不需要像JDK1.7的实现那样重新计算hash,只需要看看原来的hash值新增的那个bit是1还是0就好了,是0的话索引没变,是1的话索引变成“原索引+oldCap”
前人的脑子是怎么长的,才能想出这么骚的操作呢
推荐阅读:https://www.cnblogs.com/leesf456/p/5242233.html