HashMap的默认容量为什么要设置16?

 

在HashMap中,indexFor方法其实主要是将hashcode换成链表数组中的下标。

static int indexFor(int h, int length) {
    return h & (length-1);
}

这里实际就是取模。

用位运算是因为它比取模运算效率要高很多,因为它是直接对内存数据操作,不需要转成十进制,因此处理速度非常快。

但是需要length是2^n, 这样才满足:

X % 2^n = X & (2^n – 1)

所以,HashMap的容量一定要是2^n。

那么为什么要是16呢?而不是4,8 ,32呢?

这应该是经验值,需要在效率和内存使用上做一个权衡。这个值不能太大,也不能太小。

太小了就可能会频繁的发生扩容,影响效率;太大了又浪费空间,不划算。

所以,16作为一个经验值就被采用了。

那么HashMap如何保证其容量一定可以是2^n呢?

HashMap在两个可能改变其容量的地方都做了兼容处理:

  1. 指定容量初始化值时;

  2. 扩容时;

指定容量初始化值时

HashMap根据用户传入的初始化容量,利用无符号右移和按位或运算等方式计算出第一个大于该数的2的幂。

看一下JDK是如何找到比传入的指定值大的第一个2的幂的:

int n = cap - 1;
//step1 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;//step2

上面的算法目的挺简单,就是:根据用户传入的容量值(代码中的cap),通过计算,得到第一个比他大的2的幂并返回。

step1具体 怎么理解呢?其实是对一个二进制数依次向右移位,然后与原值取或。其目的对于一个数字的二进制,从第一个不为0的位开始,把后面的所有位都设置成1。随便拿一个二进制数,套一遍上面的公式就发现其目的了:

1100 1100 1100 >>>1 = 0110 0110 0110
1100 1100 1100 | 0110 0110 0110 = 1110 1110 1110
1110 1110 1110 >>>2 = 0011 1011 1011
1110 1110 1110 | 0011 1011 1011 = 1111 1111 1111
1111 1111 1111 >>>4 = 1111 1111 1111
1111 1111 1111 | 1111 1111 1111 = 1111 1111 1111

Step 2 比较简单,就是做一下极限值的判断,然后把Step 1得到的数值+1。

另外注意:

在JDK 1.7和JDK 1.8中,HashMap初始化这个容量的时机不同

JDK 1.8中,在调用HashMap的构造函数定义HashMap的时候,就会进行容量的设定。

而在JDK 1.7中,要等到第一次put操作时才进行这一操作。

总之,HashMap根据用户传入的初始化容量,利用无符号右移和按位或运算等方式计算出第一个大于该数的2的幂

扩容

除了初始化的时候会指定HashMap的容量,在进行扩容的时候,其容量也可能会改变。

HashMap有扩容机制,就是当达到扩容条件时会进行扩容。

HashMap的扩容条件就是当HashMap中的元素个数(size)超过临界值(threshold)时就会自动扩容。

在HashMap中,threshold = loadFactor * capacity。

loadFactor是装载因子,表示HashMap满的程度,默认值为0.75f,设置成0.75有一个好处,那就是0.75正好是3/4,而capacity又是2的幂。

所以,两个数的乘积都是整数。

下面是HashMap中的扩容方法(resize)中的一段:

if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
    oldCap >= DEFAULT_INITIAL_CAPACITY)
    newThr = oldThr << 1; // double threshold
}

从上面代码可以看出,扩容后的table大小变为原来的两倍,这一步执行之后,就会进行扩容后table的调整,这部分非本文重点,省略。

所以,通过保证初始化容量均为2的幂,并且扩容时也是扩容到之前容量的2倍,所以,保证了HashMap的容量永远都是2的幂。

总结

HashMap作为一种数据结构,元素在put的过程中需要进行hash运算,目的是计算出该元素存放在hashMap中的具体位置。

hash运算的过程其实就是对目标元素的Key进行hashcode,再对Map的容量进行取模,而JDK 的工程师为了提升取模的效率,使用位运算代替了取模运算,这就要求Map的容量一定得是2的幂。

而作为默认容量,太大和太小都不合适,所以16就作为一个比较合适的经验值被采用了。

为了保证任何情况下Map的容量都是2的幂,HashMap在两个地方都做了限制。

首先是,如果用户制定了初始容量,那么HashMap会计算出比该数大的第一个2的幂作为初始容量。

另外,在扩容的时候,也是进行成倍的扩容,即4变成8,8变成16。

 

参考:https://mp.weixin.qq.com/s/ktre8-C-cP_2HZxVW5fomQ

posted @ 2021-08-23 23:08  Vincent-yuan  阅读(2305)  评论(0编辑  收藏  举报