使用 HashMap 存一万条数据,构造时传 10000 还会触发扩容吗?
问题
向 HashMap 中存 10000 条数据,初始化时,构造方法传值 10000,会触发扩容吗?
Map<String,String> map = new HashMap<>(10000);
分析
乍一看
肯定会触发扩容呀,因为 HashMap 中有个负载因子默认为 0.75,就是说存储的数量超过容量的 75% 就会触发扩容,put 到后 25% 的数据时,肯定就会触发扩容。但事实真是这样吗?源码中有我们想知道的一切,真相只有一个。
分析源码
HashMap 的初始化
在 HashMap 中,提供了一个指定初始容量的构造方法 HashMap(int initialCapacity),这个方法再通过 DEFAULT_LOAD_FACTOR 调用 HashMap 另一个构造方法,初始化 loadFactor 为 0.75。
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
public HashMap(int initialCapacity, float loadFactor) {
......
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}
构造方法初始化了两个成员变量 threshold 和 loadFactor,其中 threshold 就是用来存储触发 HashMap 扩容的阈值,也就是说,当 HashMap 存储的数据量达到 threshold 时,就会触发扩容。
从改造方法中可以看出,threshold 并没有直接使用传入的 initialCapacity 作为扩容阈值,而是通过 tableSizeFor 方法处理后再赋值给 threshold。
static final int tableSizeFor(int cap) {
int n = cap - 1;
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;
}
这啥呀?看不懂也没关系,我来解释。tableSizeFor 的作用就是寻找大于 cap 的 2 的整数次方,例如如果传入 10,经过 tableSizeFor 处理后返回 16。至于为什么这样做,不在此处详细讨论。
回归正题
还有一个问题,一直在说 threshold 是触发扩容的阈值,即 (initialCapacity * loadFactor),但是我们再构造方法中使用 tableSizeFor 初始化 threshold,并没有用到 loadFactor,其实这一步被移交给 resize 方法实现了,因为我们并没有在构造方法中初始化哈希表数组,扩容阈值 threshold = initialCapacity * loadFactor,这一步在 resize 中完成。
如果我们从外部传递进来 10000 初始化 initialCapacity ,实际上经过 tableSizeFor 方法处理之后,最终 threshold 就会变成 2 的 14 次幂 16384,再在 resize 方法中乘上负载因子 0.75,实际在不触发扩容的前提下,可存储的数据容量是 12288(16384 * 0.75),用来存放 10000 条数据,绰绰有余,所以并不会触发扩容。
initialCapacity
关于如何选择 initialCapacity,我们看看阿里巴巴 Java 开发规范,规范要求在初始化 HashMap 时,必须指定 initialCapacity,因为这样可以减少 resize 次数,提高程序效率。因为 threshold = initialCapacity * loadFactor,所以 initialCapacity = (需要存储元素个数 / loadFactor) + 1。
示例
想要使用 HashMap 存放 10000 条数据,应该设置 initialCapacity = 10000 / 0.75 + 1 = 13334,然后哈希表容量会被 tableSizeFor 方法调整到 16384(2^14),threshold = 16384 * 0.75 = 12288 足够存储 10000 条数据而不会触发扩容。
总结
- HashMap 构造方法传递的 initialCapacity 实际表示哈希表的容量,不代表扩容阈值。
- 构造方法传递的 initialCapacity,会被 tableSizeFor 方法调整为大于它的 2 的整数次方。
- 如果使用 initialCapacity 进行初始化,HashMap 是否扩容,由 threshold 决定,扩容阈值 threshold = initialCapacity * loadFactor。
- 在初始化 HashMap 时,必须指定 initialCapacity 来提升效率,initialCapacity = (需要存储元素个数 / loadFactor(默认0.75)) + 1。