至少 HashMap 是数组。
一个直击灵魂的问题出现了,初始化的时候表大一点好还是小一点好?
Java 中数组最大是多少?看一下数组的 length 属性就可以了。
——它的 length 属性是 32 位的有符号整数,那么取值范围是 -2^31 到 2^31-1 ,最大是 2GB。
为什么 length 的属性不是 long 呢?
——如果它是long型的,那么最大长度是 2 的 63 次幂。目前内存没那么大。
说回 HashMap 的初始化,Java给出的存储结构肯定是要即适用于大量数据也要适用于少量数据。调试的时候一个工具类直接把内存占满了就不合适了。
所以初始化的时候表不能太大。
那多大?
这就要说回小表了,小表有什么问题?
—— 哈希碰撞概率大幅度上升。
如果表大小是 1 ,那么每一个节点都一定碰撞,整个数据结构退化成链表。
并且如果表大小固定,在使用的过程中只要插入的数据足够多,后期使用的时候又退化成链表了。
这个逻辑是这样的,
- 不可能初始化一个巨大的表
- 所以要初始化一个小表
- 不论 hashcode 有多么精致,表小就说明后期一定会产生大量的 hash 碰撞
- 扩容
因为我们不得不用小表,所以在表中数据到达一定数量的时候一定要扩容,不然毫无意义。
在接受了小表+扩容这个解决的前提之下
小表的大小是多少?
—— 16 ,一个神奇的魔法数。
/**
* 表的大小一定要是2的幂
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
Joshua Bloch
2 的幂可以给后续的某些操作提供便利性。可是,
- 2 的幂有哪些好处?
- 为什么就是 16 呢?8 ?32 ?
先解决第一个问题。
首先方便算扩容。
new = old << 1
这么做直接就是 x 2 ,算得快。
再说扩容几倍合适?一般地,容量 x 2 怎么着也是足够的了。
第二个是方便算 i 。从 hashcode 里获得了一个大数字要变成 i , 这个 i 是 0 到长度减一之内的一个数字。怎么做?
—— mod。
i = hash % (tab.length - 1)
然而,聪明的小朋友会发现这个取余操作非常慢。事实上可以用与操作优化,
i = (tab.length - 1) & hash
这是一种取余的优化方案。
这是因为如果 n 是 2 的幂,这种方式正好可以割下了尾部的几个连续的二进制位数。这个逻辑和掩码一模一样。如下所示,
hash = 01001111
n = 00001000
n - 1 = 00000111
i = (n - 1) & hash = 00000111
因为 n 是 2 的幂, 所以可以完整地切割尾部。i 可以取 0000 到 0111 中的所有值。
可如果 n 是别的类型的数?
hash = 01001111
n = 00000111
n - 1 = 00000110
i = (n - 1) & hash = 00000110
这漏了一位。
i 只能那个取 0000 到 0111 中的所有偶数。这意味着 Table 一半的容量永远空。
这两个好处一结合,所以 n 为 2 的幂。
第二个问题要简单的多。
首先,扩容这个操作到底在做什么?
不仅仅是 Table 增加一倍。
之前定位算法是 ( tab.length - 1 ) & hash 。
现在 n 变成 2n了。之前存储的数据全都要重新用 2n 来算 i 重新定位重新处理哈希碰撞。
(事实上,当 n 变成 2n 之后,旧数据放置在同一个 i 上的概率是 50% ,另外 50% 是放到 n + i 上面)
扩容是非常麻烦的,麻烦在于要重新算 hash 算 i 。
所以尽量别扩容。
把表的大小设置成 2 或者 4 ,是不是给自己找麻烦?觉得自己电脑很牛逼就喜欢扩容,没意义的。
HashMap 给的答案是 16 ,因为他们觉得 8 也小。
一定的经验参数,又综合了各种考虑,定表大小初始值为16。
下一节要开始说 1.7 的具体实现了。基本上看到现在能明白的,就已经对 1.7 的逻辑了如指掌了,下一节了解一下专有名词。