HashMap中的hash算法总结

参考: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
}

为什么要无符号右移16位后做异或运算

根据上面的说明我们做一个简单演练

HashMap中的hash算法总结_第1张图片

将h无符号右移16为相当于将高区16位移动到了低区的16位,再与原hashcode做异或运算,可以将高低位二进制特征混合起来

从上文可知高区的16位与原hashcode相比没有发生变化,低区的16位发生了变化

我们可知通过上面(h = key.hashCode()) ^ (h >>> 16)进行运算可以把高区与低区的二进制特征混合到低区,那么为什么要这么做呢?

我们都知道重新计算出的新哈希值在后面将会参与hashmap中数组槽位的计算,计算公式:(n - 1) & hash,假如这时数组槽位有16个,则槽位计算如下:

HashMap中的hash算法总结_第2张图片

仔细观察上文不难发现,高区的16位很有可能会被数组槽位数的二进制码锁屏蔽,如果我们不做刚才移位异或运算,那么在计算槽位时将丢失高区特征

也许你可能会说,即使丢失了高区特征不同hashcode也可以计算出不同的槽位来,但是细想当两个哈希码很接近时,那么这高区的一点点差异就可能导致一次哈希碰撞,所以这也是将性能做到极致的一种体现

使用异或运算的原因

 异或运算能更好的保留各部分的特征,如果采用&运算计算出来的值会向0靠拢,采用|运算计算出来的值会向1靠拢

为什么槽位数必须使用2^n (重点)

1、为了让哈希后的结果更加均匀

这个原因我们继续用上面的例子来说明

假如槽位数不是16,而是17,则槽位计算公式变成:(17 - 1) & hash

HashMap中的hash算法总结_第3张图片

从上文可以看出,计算结果将会大大趋同,hashcode参加&运算后被更多位的0屏蔽,计算结果只剩下两种0和16,这对于hashmap来说是一种灾难

上面提到的所有问题,最终目的还是为了让哈希后的结果更均匀的分部,减少哈希碰撞,提升hashmap的运行效率

 

自己的理解: 

1. HashMap采用的是懒加载,在创建好map的时候,并没有初始化 Node[] tab,在第一次put的时候才会初始化这个tab

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;
  • 2.1 在算出hash值之后,计算这个key落在哪一个桶上面,方法有2中,一种是&,一种是%,这两种方法都可行,但是效率不一样,请看下面的的测试(抄作业的测试),这个差的还是挺多的,尤其是在map发生扩容的时候,需要重新计算所有key的hash值放到新的hash桶上面,这个时候差别的更大了

有人怀疑两种运算效率差别到底有多少,我做个测试:

    /**
     * 
     * 直接【求余】和【按位】运算的差别验证
     */
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.2 下面讲一下为什么HashMap的初始值是16,而不是15,14,17呢?

首先 这个长度应该是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

  • 2.3   if ((e.hash & oldCap) == 0)              这个地方是源码的骚操作,在扩容的时候,需要把原来map中的元素移到新生成的hash桶中,那么就需要按照 hash & (32-1) 这样再算一次,你会发现,只会有2种情况,要么还是原来的位置 5 ,要么就是 5 + 16 (假设原来是16大小) , 在源码中 这帮大牛并没有这样做,而是用 hash & 16== 0来判断,那么为什么呢?这样算出来的能行么?
 // 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;
    }
}

HashMap中的hash算法总结_第4张图片

元素在重新计算hash之后,因为n变为2倍,那么n-1的mask范围在高位多1bit,因此,我们在扩充HashMap的时候,不需要像JDK1.7的实现那样重新计算hash,只需要看看原来的hash值新增的那个bit是1还是0就好了,是0的话索引没变,是1的话索引变成“原索引+oldCap”

HashMap中的hash算法总结_第5张图片

前人的脑子是怎么长的,才能想出这么骚的操作呢

推荐阅读:https://www.cnblogs.com/leesf456/p/5242233.html

你可能感兴趣的:(学习源码)