前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >原创|如果懂了HashMap这两点,面试就没问题了

原创|如果懂了HashMap这两点,面试就没问题了

作者头像
每天晒白牙
发布2020-08-21 01:31:43
4180
发布2020-08-21 01:31:43
举报
文章被收录于专栏:每天晒白牙

HashMap 是后端面试的常客,比如默认初始容量是多少?加载因子是多少?是线程非安全的吗?put 操作过程复述下?get 操作复述下?在 jdk 1.7 和 1.8 实现上有什么不同?等等一系列问题,可能这些问题你都能对答如流,说明对 HashMap 还是比较理解的,但最近我们团队的同学做了一个技术分享,其中有几点我挺有收获的,我给大家分享下

我们每周五都会进行技术分享,大家轮流分享,其实这种机制挺好的,大家坐在一起深入讨论一个知识点,进行思维的碰撞,多赢

抛出两个问题,看你能否回答出来?

  1. 如何找到比设置的初始容量值大的最小的 2 的幂次方整数?
  2. HashMap 中对 key 做 hash 处理时,做了什么特殊操作?为什么这么做?

先自己思考下,再往下阅读效果更佳哦!

下面的分析都是针对 jdk 1.8

分析

问题1:如何找到比设置的初始容量值大的最小的 2 的幂次方整数?

我们在用 HashMap 的时候,如果用默认构造器,就会建一个初始容量为 16,加载因子为 0.75 的 HashMap。这样做有个缺点,就是在数据量比较大的时候,会进行频繁的扩容操作,扩容会发生数据的移位,为了避免扩容,提高性能,我们习惯预估下容量,然后通过带容量的构造器创建,看下源码

代码语言:javascript
复制
public HashMap(int initialCapacity, float loadFactor) {
    ...
    // 如果设置的初始容量大于最大容量就默认为最大容量 2^30     
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    ...
    this.loadFactor = loadFactor;
    // tableSizeFor 方法主要就是计算比给定的初始容量值大的最小的 2 的幂次方整数
    this.threshold = tableSizeFor(initialCapacity);
}

通过源码我们可知,容量最大值为 2^30,也就是说 HashMap 的数组部分的长度的范围为[0,2^30],然后计算比初始容量大的最小的2的幂次方整数,其中 tableSizeFor 方法是重点,我们看下源码

代码语言:javascript
复制
// Returns a power of two size for the given target capacity
static final int tableSizeFor(int cap) {
    int n = cap - 1;
    n |= n >>> 1;
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

这个方法设计的非常巧妙,因为 HashMap 要保证容量是 2 的整数次幂,该方法实现的效果就是如果你输入的 cap 本身就是偶数,那么就返回 cap 本身,如果输入的 cap 是奇数,返回的就是比 cap 大的最小的 2 的整数次幂

为什么容量要是 2 的整数次幂?

因为获取 key 在数组中对应的下标是通过 key 的哈希值与数组长度 -1 进行与运算,如:tab[i = (n - 1) & hash]

  1. n 为 2 的整数次幂,这样 n-1 后之前为 1 的位后面全是 1,这样就能保证 (n-1) & hash 后相应的位数既可能是 0 又可能是 1,这取决于 hash 的值,这样能保证散列的均匀,同时与运算效率高
  2. 如果 n 不是 2 的整数次幂,会造成更多的 hash 冲突

该方法首先执行了 cap -1 操作,这样做的好处是避免输入的 cap 是偶数,最后计算的数是 cap 的 2 倍的情况,因为设置的偶数 cap 已经满足 HashMap 的要求了,没有必要初始化一个 2 倍容量的 HashMap 了,看不明白不急后面有示例分析

前面我们已经介绍 HashMap 的最大容量为 2^30,所以容量最大就是 30 bit 的整数,我们就用 30 位的一个数演示下算法中的移位取或操作,假设 n = 001xxx xxxxxxxx xxxxxxxx xxxxxxxx (x 代表该位上是 0 还是 1 我们不关心)

第一次右移 n |= n >>> 1 ,该操作是用 n 本身 和 n 右移 1 位后的数进行或操作,这样可以实现把 n 的最高位的 1 紧邻的右边一位也置为 1

代码语言:javascript
复制
n       001xxx xxxxxxxx xxxxxxxx xxxxxxxx
n >>> 1 0001xx xxxxxxxx xxxxxxxx xxxxxxxx
| 或操作 0011xx xxxxxxxx xxxxxxxx xxxxxxxx
结果就是把 n 的最高位为 1 的紧邻的右边的 1 位也置为了 1,这样高位中有连续两位都是 1

第二次右移 n |= n >>> 2

代码语言:javascript
复制
n       0011xx xxxxxxxx xxxxxxxx xxxxxxxx
n >>> 2 000011 xxxxxxxx xxxxxxxx xxxxxxxx
| 或操作 001111 xxxxxxxx xxxxxxxx xxxxxxxx
结果就是 n 的高位中有连续 4 个 1

第三次右移 n |= n >>> 4

代码语言:javascript
复制
n       001111 xxxxxxxx xxxxxxxx xxxxxxxx
n >>> 4 000000 1111xxxx xxxxxxxx xxxxxxxx
| 或操作 001111 1111xxxx xxxxxxxx xxxxxxxx
结果就是 n 的高位中有连续 8 个 1

第四次右移 n |= n >>> 8

代码语言:javascript
复制
n        001111 1111xxxx xxxxxxxx xxxxxxxx
n >>> 8  000000 00001111 1111xxxx xxxxxxxx
| 或操作  001111 11111111 1111xxxx xxxxxxxx
结果就是 n 的高位中有连续 16 个 1

第五次右移 n | n >>> 16

代码语言:javascript
复制
n        001111 11111111 1111xxxx xxxxxxxx
n >>> 16 000000 00000000 00001111 11111111
| 或操作  001111 11111111 11111111 11111111
结果就是 n 的高位1后面都置为 1

最后会对 n 和最大容量做比较,如果 >= 2^30,就取最大容量,如果 < 2^30 ,就对 n 进行 +1 操作,因为后面位数都为1,所以 +1 就相当于找比这个数大的最小的 2的整数次幂

011111 11111111 11111111 11111111,这个值就是比给的值大的最小的 2 的整数次幂

下面我们用一个具体说演示下,比如 cap = 18

cap为18

我们输入的是 18,输出的是 32,正好是比 18 大的最小的 2 整数次幂

如果 cap 本身就为 2的整数次幂,输出结果为什么?

cap为16

通过演示可见,cap 本身就是 2 的整数次幂的输出结果为其本身

上面还遗留了个问题,就是先对 cap -1,我解释说为了避免输出的是偶数,最后计算的结果为 2*cap,浪费空间,看下面的演示

cap为16未减一

通过演示,我们可以看出,输入的是 16,最后计算的结果却是 32,这就会浪费空间了,所以说算法很牛,先对 cap 做了减一操作

问题2:HashMap 中对 key 做 hash 处理时,做了什么特殊操作?为什么这么做?

首先我们知道 HashMap 在做 put 操作的时候,会先对 key 做 hash 操作,直接定位到源码位置

代码语言:javascript
复制
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

可以看到再对 key 做 hash 操作时,执行了 (h = key.hashCode()) ^ (h >>> 16)

代码语言:javascript
复制
原 hashCode 值: 10110101 01001100 10010101 11011111
右移 16 位后的值: 00000000 00000000 10110101 01001100 
异或后的值:      10110101 01001100 00100000 10010011

这个操作是把 key 的 hashCode 值与 hashCode 值右移 16 位做异或(不同为 1,相同为 0),这样就是把哈希值的高位和低位一起混合计算,这样就能使生成的 hash 值更离散

这里需要我解释下,通过前面的介绍,我们知道数组的容量范围是 [0,2^30],这个数还是比较大的,平时使用的数组容量还是比较小的,比如默认的大小 16,假设三个不同的 key 生成的 hashCoe 值如下所示:

19305951 00000001 00100110 10010101 11011111

128357855 00000111 10100110 10010101 11011111

38367 00000000 00000000 10010101 11011111

他们三个有个共同点是低 16 位完全一样,但高 16 位不同,当计算他们在数组中所在的下标时,通过 (n-1)&hash,这里 n 是 16,n-1=15,15 的二进制表示为

00000000 00000000 00000000 00001111

用 19305951、128357855、38367 都与 15 进行 & 运算,结果如下

hash冲突

通过计算后发现他们的结果一样,也就是说他们会被放到同一个下标下的链表或红黑树中,显然不符合我们的预期

所以对 hash 与其右移 16 位后的值进行异或操作,然后与 15 做与运算,看 hash 冲突情况

解决hash冲突

可见经过右移 16位后再进行异或操作,然后计算其对应的数组下标后,就被分到了不同的桶中,解决了哈希碰撞问题,思想就是把高位和低位混合进行计算,提高分散性

总结

其实 HashMap 还有很多值得研究的点,上面两个点搞明白后,会感叹作者写代码的能力真是牛,我们在工作中要借鉴这些思想,希望通过我的讲解,你能掌握这两个知识点,如果有不懂的可以留言或私聊我

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2019-11-07,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 每天晒白牙 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 抛出两个问题,看你能否回答出来?
  • 分析
    • 问题1:如何找到比设置的初始容量值大的最小的 2 的幂次方整数?
      • 问题2:HashMap 中对 key 做 hash 处理时,做了什么特殊操作?为什么这么做?
        • 总结
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档