当我们创建一个HashMap容器时, 有时为了节省空间, 会指定容器的默认大小.如下图, 虽然我指定了HashMap默认大小是12, 但实际HashMap默认大小是16, 因为设置HashMap默认大小必须是2的幂次方法. 所以实际大小是16, 但是为什么是这样呢? 下面我们来分析一下.
首先我看下构造方法里,请看下图. 在HashMap(int initialCapacity)构造方法中, 实际调用this(initialCapacity, DEFAULT_LOAD_FACTOR)这个构造方法, DEFAULT_LOAD_FACTOR就是负载因子(0.75), 这个是后期计算链表数组大小使用的.我们这里不需要关心.
然后我们看下this(initialCapacity, DEFAULT_LOAD_FACTOR)这个构造方法, 发现它调用了tableSizeFor方法
在我们学习tableSizeFor方法之前, 我们需要了解几个知识点, <<(左移), >>(右移), >>>(无符号右移), &(与), |(或), ^(非), 这里大家自己查阅一下资料吧, 我就不在这里讲解了. 下面我们来分析一下tableSizeFor方法. 下图是我们要讲解的tableSizeFor方法和该方法中使用到的常量值等, 注释信息的一个翻译(翻译的不好大家见谅(●'◡'●))
1.首先我们先说下 int n = cap -1; 这里为什么要减一?
因为在HashMap中, 初始容量大小默认为2的幂次方, 即1, 2,4,8,16,32....一直到MAXIMUM_CAPACITY,
如果不减一, 当cap=1时, 初始容量为2, cap=2时初始容量=4(其实2就够用), cap=4时, 初始容量=8(其实4就够用), 这样会导致空间的浪费.
2. 这时候可能有人会问了, 为什么不减一, cap=1的时候, 初始容量就是2了呢?
接下来我们讲一下>>>(无符号右移了)和|(或)运算
注: 我转成二进制只写了8位, 如果你是32位系统, 就是32个0或1, 如果你是64位系统就是64个0或1, 这里我们用不到那么多位,就不写那么长了.只用了8位.
2.1 首先我们先说下, 不减一的情况下
// 例 cap = 4;
int n = cap; // 第一步: n = 4, 转换成二进制, 0000 0100
n |= n >>> 1; // 第二步: 相当于n = n | n >>> 1;
// n >>> 1 相当于 0000 0100 往右移一位, 变成 0000 0010
// n | n >>> 1 相当于 0000 0100 | 0000 0010, 变成 0000 0110 转成十进制等于6
// |(或)的意思就是有1则1,
n |= n >>> 2; // 第三步: 相当于n = n | n >>> 2;
// n >>> 2 相当于 0000 0110 往右移两位, 变成0000 0001
// n | n >>> 2 相当于 0000 0110 | 0000 0001, 变成 0000 0111 转成十进制等于7
n |= n >>> 4; // 第四步: 相当于n = n | n >>> 4;
// n >>> 4 相当于 0000 0111 往右移四位, 变成0000 0000
// n | n >>> 2 相当于 0000 0111 | 0000 0000, 变成 0000 0111 转成十进制等于7
n |= n >>> 8; //同第三步
n |= n >>> 16; //同第三步
// 下面这部是两个三元运算, 我们给拆分一下
// 即:
// 公式1: b = n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1
// n >= MAXIMUM_CAPACITY吗? 大于 b = MAXIMUM_CAPACITY 小于b = n+1, b=8
//
// 公式2: n = n < 0) ? 1 : b
// n < 0 吗 ? 小于n = 1, 大于n = b 即n = 8
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
通过以上解析可以看出, 为什么要减一了. 因为, 如果不减一, 当我们初始容器大小是2的幂次方时, 实际容器初始化大小是2的幂次方+1, 所以这里要对我们输入的初始化容器大小减一.
2.2 然后我们说下, 减一的情况下
// 例 cap = 4;
int n = cap-1; // 第一步: n = 3, 转换成二进制, 0000 0011
n |= n >>> 1; // 第二步: 相当于n = n | n >>> 1;
// n >>> 1 相当于 0000 0011 往右移一位, 变成 0000 0001
// n | n >>> 1 相当于 0000 0011 | 0000 0001, 变成 0000 0011 转成十进制等于3
// |(或)的意思就是有1则1,
n |= n >>> 2; // 第三步:相当于n = n | n >>> 2;
// n >>> 2 相当于 0000 0011 往右移两位, 变成0000 0000
// n | n >>> 2 相当于 0000 0011 | 0000 0000, 变成 0000 0011 转成十进制等于3
n |= n >>> 4; //同第三步
n |= n >>> 8; //同第三步
n |= n >>> 16; //同第三步
// 下面这部是两个三元运算, 我们给拆分一下
// 即:
// 公式1: b = n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1
// n >= MAXIMUM_CAPACITY吗? 大于 b = MAXIMUM_CAPACITY 小于b = n+1, b=4
//
// 公式2: n = n < 0) ? 1 : b
// n < 0 吗? 小于n = 1, 大于n = b 即n=4
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
4. 总结: 通过我们对tableSizeFor方法的解析,我们明白了, 原来HashMap是通过>>>(无符号右移) 2的幂次方数(2, 4, 8, 16), 加上|(或)运算来保证初始大小总是2的幂次方, 所以当我们通过构造方法设置了HashMap的初始大小以后, 如果我们设置的不是2的幂次放, 也会通过tableSizeFor将初始容量变成2的幂次方,即我们new HashMap(12), 实际HashMap的大小是16. 通过对cap减一保证HashMap使用合理的初始容量, 达到空间的利用率.
5. 课外知识: 看了上面的讲解, 是不是可以自己计算出 MAXIMUM_CAPACITY的大小了, MAXIMUM_CAPACITY = 1 << 30 我们这里用32位的二进制来计算, 64位无非就是在前面再加32个0, 我们这里用不到就不加了.
即: 0000 0000 0000 0000 0000 0000 0000 0001 左移30位 变成 0100 0000 0000 0000 0000 0000 0000 0000
0100 0000 0000 0000 0000 0000 0000 0000 转成十进制等于1073741824
6. 疑问点:
问题1: 为什么HashMap的初始容量, 一定要是2的幂次方呢? 为什么要这么设计呢?
【HashMap】为什么HashMap的大小一定要是2的幂次方呢?_尛_的博客-CSDN博客
问题2: 上面我们分析的只是初始容量值的一个计算, 那HashMap什么时候初始化的, 并指定容器的大小呢?