写在前面
通过本文可以了解 HashMap
及 ArrayList
的扩容策略以及如何设置初始值大小。
为什么要设置初始值大小?
底层使用数组实现的集合类,比如:HashMap
、ArrayList
,都会存在扩容的问题,当数组空间不够用时,需要对数组进行动态扩容,不然就会造成 ArrayIndexOutOfBoundsException
异常。
如果在创建集合时,就已经知晓集合的大小,那么就可以事先设置集合底层数据的大小,这样就可以避免添加元素的过程中自动扩容,从而达到提高性能的目的。
以 HashMap
为例,已知集合有两个元素待加入,那么下面的代码有没有问题?
public static void main(String[] args) throws Exception {
HashMap<String, Integer> map = new HashMap<>(2);
map.put("1", 1);
map.put("2", 1);
}
可以打印一下数组长度,看看过程中 HashMap
有没有自动进行扩容
public class ListsTest {
public static void main(String[] args) throws Exception {
HashMap<String, Integer> map = new HashMap<>(2);
map.put("1", 1);
displayMapLength(map);
map.put("2", 1);
displayMapLength(map);
}
public static void displayMapLength(HashMap<?, ?> map) throws Exception {
Field field = HashMap.class.getDeclaredField("table");
field.setAccessible(true);
Object[] elementData = (Object[]) field.get(map);
System.out.println(elementData == null ? 0 : elementData.length);
}
}
输出结果如下,结果很明显,进行了一次扩容。
2
4
将 HashMap
的初始容量设置为期望集合大小是不对的,会导致扩容,那么应该设置为多少才合适呢?这就需要来了解一下 HashMap
的扩容机制。
扩容机制
几个概念:
- capacity:容量,
HashMap
的内部实现使用数组进行数据存储,capacity
就是数组的长度,默认为16
。 - size:容器内已存储数据的数量
- loadFactor:扩容因子,一个
0 - 1
之间的数值,默认为0.75
- threshold:扩容界限,
threshold = capacity * loadFactor
向下取整
扩容策略:
向容器新增一个数据,size
加 1,当 size > threshold
时,执行扩容,capacity = capacity * 2
每次扩容为原来的一倍。
用上面的例子再验证一下扩容机制
初始态:
capacity = 2
threshold = 2 * 0.75 = 1
新增第一个元素,size = 1
,size > threshold
不满足,不执行扩容。
新增第二个元素,size = 2
,size > threshold
满足条件,执行扩容,扩容后
capacity = capacity * 2 = 4
threshold = 4 * 0.75 = 3
如果继续向容器新增数据,在新增第 4 个元素时会导致下一次扩容。
如何设置初始值
了解了扩容机制后,现在回到开始的问题,已知集合有两个元素待加入,HashMap
的容量初始化为多少最合适?
只需要满足 capacity * 0.75 >= expectedSize
即可,所以容量设置可以使用如下公式
capacity = (int) Math.ceil((float) expectedSize / 0.75F)
当 expectedSize = 2
时,使用公式计算得到 capacity = 3
,再重新跑一遍之前的程序
public class ListsTest {
public static void main(String[] args) throws Exception {
HashMap<String, Integer> map = new HashMap<>(3);
map.put("1", 1);
displayMapLength(map);
map.put("2", 1);
displayMapLength(map);
}
}
结果输出为
4
4
很明显,没有进行扩容,但我们明明设置的是 3,但实际容量却是 4,这是因为 HashMap
在构造函数中对容量进行多一次的计算,计算后的容量一定是 2n。
如何设置 ArrayList 初始值
ArrayList
的内部实现与 HashMap
是不一样的,扩容机制也不一样。
ArrayList
初始化时,设置容量为多少,数组长度即为多少,不会进行多一次计算。ArrayList
没有loadFactor
扩容因子,数组长度不够,才进行扩容。ArrayList
每次扩容,增加原来的一半容量。
所以,如果有两个元素待加入列表,那么直接创建一个初始容量为 2 的列表即可。
Lists & Maps 工具类
如果觉得自己去理清这些东西比较麻烦的话,有人已经帮我们把这些都考虑到了,使用现成的工具类即可解决这些问题。
在 Guava
提供的工具类中,Lists
及 Maps
中提供了相应的 API
Map<String, Integer> map = Maps.newHashMapWithExpectedSize(2);
List<String> list = Lists.newArrayListWithCapacity(2);