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

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

咱们每周五都会进行技术分享,你们轮流分享,其实这种机制挺好的,你们坐在一块儿深刻讨论一个知识点,进行思惟的碰撞,多赢面试

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

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

先本身思考下,再往下阅读效果更佳哦!算法

下面的分析都是针对 jdk 1.8后端

分析

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

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

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 方法是重点,咱们看下源码安全

// 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 自己就是 2 的整数次幂,那么就返回 cap 自己,若是输入的 cap 不是 2 的整数次幂,返回的就是比 cap 大的最小的 2 的整数次幂性能

为何容量要是 2 的整数次幂?this

由于获取 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 是 2 的整数次幂,最后计算的数是 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

n       001xxx xxxxxxxx xxxxxxxx xxxxxxxx
n >>> 1 0001xx xxxxxxxx xxxxxxxx xxxxxxxx
| 或操做 0011xx xxxxxxxx xxxxxxxx xxxxxxxx
结果就是把 n 的最高位为 1 的紧邻的右边的 1 位也置为了 1,这样高位中有连续两位都是 1

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

n       0011xx xxxxxxxx xxxxxxxx xxxxxxxx
n >>> 2 000011 xxxxxxxx xxxxxxxx xxxxxxxx
| 或操做 001111 xxxxxxxx xxxxxxxx xxxxxxxx
结果就是 n 的高位中有连续 4 个 1

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

n       001111 xxxxxxxx xxxxxxxx xxxxxxxx
n >>> 4 000000 1111xxxx xxxxxxxx xxxxxxxx
| 或操做 001111 1111xxxx xxxxxxxx xxxxxxxx
结果就是 n 的高位中有连续 8 个 1

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

n        001111 1111xxxx xxxxxxxx xxxxxxxx
n >>> 8  000000 00001111 1111xxxx xxxxxxxx
| 或操做  001111 11111111 1111xxxx xxxxxxxx
结果就是 n 的高位中有连续 16 个 1

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

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 操做,直接定位到源码位置

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

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

原 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

用 1930595一、12835785五、38367 都与 15 进行 & 运算,结果以下

hash冲突

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

因此对 hash 与其右移 16 位后的值进行异或操做,而后与 15 作与运算,看 hash 冲突状况 解决hash冲突

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

总结

其实 HashMap 还有不少值得研究的点,上面两个点搞明白后,会感叹做者写代码的能力真是牛,咱们在工做中要借鉴这些思想,但愿经过个人讲解,你能掌握这两个知识点,若是有不懂的能够留言或私聊我

欢迎关注公众号 【天天晒白牙】,获取最新文章,咱们一块儿交流,共同进步!

相关文章
相关标签/搜索