面试逃不过的HashMap哈希原理,这一次一定要弄懂

12 篇文章 0 订阅

1. 前言

HashMap绝对是JDK源码中比较精髓的存在,除此之外还有AQS,线程池等等。但是由于HashMap是我们接触最早也是接触最多的,所以面试八股文中绝对少不了它的存在。

想要学习一门技术能够大概了解它的原理其实已经差不多了,但是面试官往往会刨根问底,追问你put流程,resize过程,为什么负载因子是0.75之类的……

其实这些都还好,最让人头疼的是哈希算法,我们来看看JDK1.8中计算下标的源码:

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

除此之外还有一句,这个hash就是上面得到的值,而n就是数组长度:

(n - 1) & hash

那么问题来了:

  • 为什么要右移16位?
  • 为什么要和自己再进行异或(^)操作?
  • 为什么要n-1?
  • 为什么是位运算而不是求模?

带着这些疑问,我们继续往下读。

2. 哈希算法的目标

我们都知道HashMap的原理,对于一个key进行哈希运算得到下标,然后把这个key对应的value存入数组中。

我们假设数组容量是16,如果这时候来了一个hashcode是9999的值怎么办?直接用9999做下标会越界,所以我们需要做进一步处理:

保证哈希运算得到的结果在容量范围内


2.1 如何保证效率?

有人说这还不简单,直接取模不就完事?

hashcode % 16

行了打完收工,我们的哈希算法完事了。如果说这是在你的业务代码里这么写,当然没什么问题。但这可是JDK源码,你业务代码运行的基石,取模运算的本质:

  1. 求整数商:c = a/b;
  2. 计算模/余数:r = a - (c*b);

可以看到,这其中包括了除法、乘法、减法运算,直接取模效率太低了,那么有没有什么方法能够取代求模呢?

当然有:

当length为2的n次方时,其实(length-1)&hashCode计算出来的值和hashCode%length是一致的

于是我们的算法得到了改进:

(length-1)&hashCode

咦?这不就是JDK源码中的(n - 1) & hash吗?除此之外,我们还有个意外收获:HashMap的容量为什么是2的n次幂。


2.2 怎么减少哈希冲突?

效率是提上来了,新的问题又来了:

String a = "张三";
String b = "李四";
String c = "王五";
String d = "赵六";
int length = 31;
System.out.println(a.hashCode() & length);
System.out.println(b.hashCode() & length);
System.out.println(c.hashCode() & length);
System.out.println(d.hashCode() & length);

假设我们有两个hashcode的值a和b,使用我们刚才推导出来的算法,计算出来的下标是这样的:
在这里插入图片描述

可以看到,张三和王五的下标发生了冲突,这个算法的似乎不太聪明的亚子,怎么办?

这个也简单,我们对哈希算法做个扰动即可。

2.3 怎么进行扰动?

事实上,当length=8时,下标运算结果取决于哈希值的低三位,当length=16时,下标运算结果取决于哈希值的低四位,以此类推,我们得出结论:

当length=2的N次方, 下标运算结果取决于哈希值的低N位。

也就是说,我们在进行(n - 1) & hash时,实际上是取低N位的值,那么如何让低N位的值分布得更加均匀呢?

用一个固定的值再去运算一次?这样做太死板了,我们需要的是更加随机,更加均匀。那么我们能不能让它对自己做点手脚?

我们知道,一个hashcode是32位,如果我们对它某些位数的值进行改动,然后再让这个改动后的值与自己进行一次运算,结果是不是就更加随机,更加均匀了?

于是我们可以将高十六位无符号右移之后,与原来的值做异或运算。这样可以让高十六位的特征与低十六位的特征进行了混合,得到的新的数值中就高位与低位的信息都被保留了。

而在这里采用异或运算而不采用& 、| 运算的原因是 异或运算能更好的保留各部分的特征,如果采用&运算计算出来的值会向1靠拢,采用|运算计算出来的值会向0靠拢。

整个过程的核心突出一个点:均匀

于是我们的算法再次得到了改进:

(hashCode ^ (hashCode >>> 16)) & (length-1)

这不就是JDK源码中的做法吗?

2.4 新的算法效果如何?

我们用刚才推导出来的算法再来运行一次:

String a = "张三";
String b = "李四";
String c = "王五";
String d = "赵六";
int length = 31;
System.out.println(a.hashCode() & length);
System.out.println(b.hashCode() & length);
System.out.println(c.hashCode() & length);
System.out.println(d.hashCode() & length);
System.out.println("--------------------");
System.out.println((a.hashCode() ^ a.hashCode() >>> 16) & length);
System.out.println((b.hashCode() ^ b.hashCode() >>> 16) & length);
System.out.println((c.hashCode() ^ c.hashCode() >>> 16) & length);
System.out.println((d.hashCode() ^ d.hashCode() >>> 16) & length);

结果很明显,计算出来的下标明显均匀了许多!
在这里插入图片描述

3. 结尾

看到这里前面提出的几个问题大家心里应该都有数了,实际上本文还有一些延伸的问题,比如:计算机是怎么进行乘除运算的?为什么位运算可以替代取模?一定要高十六位右移吗?高八位高十二位行不行?

这些问题留给读者去思考,如有问题,欢迎在评论区中留言指正!

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值