在HashMap中,indexFor方法其实主要是将hashcode换成链表数组中的下标。
代码语言:javascript复制static int indexFor(int h, int length) {
return h & (length-1);
}
这里实际就是取模。
用位运算是因为它比取模运算效率要高很多,因为它是直接对内存数据操作,不需要转成十进制,因此处理速度非常快。
但是需要length是2^n, 这样才满足:
代码语言:javascript复制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的幂的:
代码语言:javascript复制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。随便拿一个二进制数,套一遍上面的公式就发现其目的了:
代码语言:javascript复制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)中的一段:
代码语言:javascript复制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