带你走进Java集合_HashMap源码分析_分析容器大小必须是2的整数次幂原因

我们上一篇文章主要介绍了HashMap的底层数据结构、构造方法、重要的属性,在上一篇我们遗留了一个问题,那就是为什么HashMap的大小必须是2的整数次幂,这一篇文章,我们从源码的角度来解决这个问题。首先我们回顾一下上一篇文章的重点内容

1)HashMap的底层数据结构是数组+链表+红黑树,我将要有一篇文章重点讲解HashMap的链表、红黑树。

2)底层数组的容量大小必须是2的整数次幂。这篇文章重点讲解

3)扩容相关的两个重要属性loadFactor(加载因子)和threshold(阀门),其中threshold=底层数组容量大小*loadFactor;

4)HashMap构造函数中并没有初始化底层数组的大小,底层数组的大小是第一次调用put时初始化的。

讲解这个知识点之前,我们要知道.

1)调用HashMap无参构造函数HashMap(),底层数组的大小为1<<4,也就是16,2的4次幂

2)调用HashMap有参构造函数HashMap(int initialCapacity) 通过tabSizeFor计算得到大于或等于initialCapacity最接近的2的整数次幂。

例如:用户HashMap(8).通过tabSizeFor方法得到8,HashMap(7)通过tabSizeFor方法得到8,HashMap(9)通过tabSizeFor方法得到16.其中tabSizeFor的第一行代码cap-1的作用就为了防止用户给定的就是2的整数次幂,上面8,如果没有cap-1,通过下面的计算获得了16.实际上应该是8,所以知道了cap-1的作用了。

综上所述,不管是默认的,还是我们指定的大小,最终底层数组容器的大小一定为2的整数次幂,这是为什么呢?我们接下来从源码解读分析。

第一步:我们要从put的方法说起,HashMap的put源码如下:

public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

我们首先看hash(key),HashMap为了性能的,利用Hash散列存储的,但是不管hash算法如何的好,都有可能出现hash冲突,HashMap利用key的hash值的高16位与低16位进行异或来降低hash的冲突。源码如下:

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

异或的运算规律:两个操作数,相同则为0,不同则为1,举例说明:

1)2^3=1

《带你走进Java集合_HashMap源码分析_分析容器大小必须是2的整数次幂原因》

2)9^17=24

《带你走进Java集合_HashMap源码分析_分析容器大小必须是2的整数次幂原因》

HashMap通过key的hash值的高16位和低16位异或运算,是基于时间、效率多方的考量。

我们在进入putVal()方法

p = tab[i = (n - 1) & hash]

逻辑与的运算规则:两个操作数,全1则1,否则为0

putVal其他的我们暂且不分析,就分析其中上面的代码(n-1)&hash,找出下标,加入我们默认容器16,当put值时,散列的越分散越好,最好的情况就是没有碰到hash冲突,而(n-1)&hash尽可能的会均匀分布,我们上面知道,n一定是2的整数次幂。我们接下来举例说明:

如果我们默认底层数组的大小n=16,计算出的hash值分别1,2,3,4,5,6…….12,通过(n-1)&hash的结果如下:

《带你走进Java集合_HashMap源码分析_分析容器大小必须是2的整数次幂原因》

可以从以上的结果中看得出,通过(n-1)&hash可以均匀的分布。

如果n不是2的整数次幂,n=20,计算出的hash值分别1,2,3,4,5,6…….12   (n-1)&hash

《带你走进Java集合_HashMap源码分析_分析容器大小必须是2的整数次幂原因》

通过的上面的列子可以看出,如果n不是2的整数次幂,(n-1)&hash分布的很不均匀,会导致计算出的下标冲突,形成链表或者红黑树,导致性能下降,有的同学会有两个疑问?

疑问1:为什么要n-1呢?

我们上面证明了n必须是2的整数次幂,如果直接利用n&hash来计算的话,n转换成二进制,只有最高位为1,其余位数都为0,逻辑与运算&规律:全1则1,否则为0,也是为了避免分布不均的情况。

疑问2:获取数组的下标,为什么不用取余运算呢? hash%n

逻辑与运算的性能要高于取模运算,实际上HashMap中的(n-1)&hash的功能和取模运算的功能相同。

通过上面的讲解,我们是不是知道了为什么HashMap的底层容器的大小必须是2的整数次幂呢,现在我们总结一下:

1)HashMap通过key的高16位与key的低16位异或运算key.hashCode()^(key.hashCode>>>16),这样是基于从时间、效率等综合方面考虑的。

2)HashMap的底层容器大小必须是2的整数次幂,因为(n-1)&hash分布的更均匀,而(n-1)&hash是获取底层数组的下标,通过(n-1)&hash,减少冲突。只有是2的整数次幂,(n-1)&hash才能起到很好的作用。

3)(n-1)&hash和取余(%)运算达到同样的效果,但是前者效率远高于后者。

4)彻底理解底层数组容器的大小必须是2的整数次幂的源码(n-1)&hash进行分析的

    原文作者:java集合源码分析
    原文地址: https://blog.csdn.net/shaotianqiang/article/details/80802364
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞