目录
参考文章:
ArrayList 源码 & 扩容机制分析
1.ArrayList 的 3 种创建方式
在讲解 ArrayList 的扩容机制之前,先来看 ArrayList 的 3 种创建方式,即对应 3 个构造函数:
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
//...
//默认初始容量大小
private static final int DEFAULT_CAPACITY = 10;
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
//1.创建时指定容量大小
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
//2.无参构造函数,使用初始容量 10 来构造一个空列表
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
//3.构造包含指定 collection 元素的列表,这些元素利用该集合的迭代器按顺序返回
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
if ((size = elementData.length) != 0) {
// c.toArray might (incorrectly) not return Object[] (see 6260652)
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// replace with empty array.
this.elementData = EMPTY_ELEMENTDATA;
}
}
//...
}
注意:
① 以无参数构造方法创建 ArrayList 时,实际上初始化赋值的是一个空数组。当真正对数组进行添加元素操作时,才真正分配容量。即向数组中添加第一个元素时,数组容量扩为 10。
② JDK 6 new 无参构造的 ArrayList 对象时,直接创建了长度是 10 的 Object[] 数组 elementData。
2.扩容相关的源代码分析
2.1.add(E e)
先来看 add(E e)
方法
//向列表末尾添加元素,添加成功时返回 true
public boolean add(E e) {
//调用了 ensureCapacityInternal 方法,确保数组下标不越界
ensureCapacityInternal(size + 1); // Increments modCount!!
//添加元素
elementData[size++] = e;
return true;
}
注意 :JDK 11 移除了
ensureCapacityInternal
方法和ensureExplicitCapacity
方法!
2.2 ensureCapacityInternal(int minCapacity)
再来看看 ensureCapacityInternal()
方法
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
private static int calculateCapacity(Object[] elementData, int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
//返回默认的容量和传入参数的较大值
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
当要向列表中添加第 1 个元素时,minCapacity 为 1,在 Math.max()方法比较后,minCapacity 变为 10。
2.3.ensureExplicitCapacity(int minCapacity)
ensureExplicitCapacity()
方法如下:
//判断是否需要扩容
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
//调用 grow 方法进行扩容
grow(minCapacity);
}
具体分析如下:
- 当添加第 1 个元素时,elementData.length 为 0,因为执行了 ensureCapacityInternal() 方法 ,所以 minCapacity 此时为 10。此时,minCapacity - elementData.length > 0成立,所以会进入 grow(minCapacity) 方法;
- 当添加第 2 个元素时,minCapacity = 2,此时 elementData.length 在添加第一个元素后扩容成 10 了。此时,minCapacity - elementData.length > 0 不成立,所以不会执行 grow(minCapacity) 方法,既不会进行扩容。添加第 3、4、···直到第 10 个元素时,依然不会执行 grow 方法,数组容量都为 10;
- 当添加第 11 个元素时,minCapacity = 11,此时 minCapacity - elementData.length = 11 - 10 > 0 成立,因此便进行扩容操作;
2.4.grow(int minCapacity)
grow(int minCapacity)
方法是 ArrayList 扩容时的核心方法,其代码如下:
//要分配的最大数组大小
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
//扩容的核心代码
private void grow(int minCapacity) {
// oldCapacity 为旧容量,newCapacity 为新容量
int oldCapacity = elementData.length;
//将 oldCapacity 右移一位,即相当于 oldCapacity / 2,整句运算式的结果就是将新容量更新为旧容量的 1.5 倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
//检查新容量 newCapacity 是否大于最小需要容量 minCapacity,若还是小于最小需要容量,那么就把最小需要容量当作数组的新容量
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
//如果 minCapacity 大于最大容量,则新容量则为`Integer.MAX_VALUE`,否则,新容量大小则为 MAX_ARRAY_SIZE 即为 Integer.MAX_VALUE - 8。
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
/*
(1) Arrays.copyOf()方法返回的数组是新的数组对象,原数组对象仍是原数组对象不变;
(2) 该拷贝不会影响原来的数组, copyOf()的第二个自变量指定要建立的新数组长度,如果新数组的长度超过原数组的长度,则保留数组默认值;
*/
elementData = Arrays.copyOf(elementData, newCapacity);
}
在上面的代码中,下面这行代码用于计算扩容后的新容量 newCapacity:
int newCapacity = oldCapacity + (oldCapacity >> 1);
ArrayList 每次扩容之后容量都会变为原来的 1.5 倍左右:
- 当 oldCapacity 为偶数时,就是 1.5 倍,例如 oldCapacity = 10,那么 newCapacity = 10 + 10 >> 1 = 15;
- 当 oldCapacity 为奇数时,则是 1.5 倍左右, 例如 oldCapacity = 15,那么 newCapacity = 15 + 15 >> 1 = 22;
有关移位运算符 >> 的具体介绍,可以参考Java 基础面试题——运算符这篇文章。
2.5.hugeCapacity(int minCapacity)
hugeCapacity(int minCapacity)
方法如下,从上面 grow() 方法的源码可以知道:如果新容量 newCapacity > MAX_ARRAY_SIZE,则执行 hugeCapacity() 方法来比较 minCapacity 和 MAX_ARRAY_SIZE:
- 如果 minCapacity 大于最大容量,则新容量 newCapacity 则为 Integer.MAX_VALUE;
- 否则,新容量 newCapacity 则为 MAX_ARRAY_SIZE 即为 Integer.MAX_VALUE - 8;
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
2.6.小结
下面简单总结一下 ArrayList 的扩容步骤:
- 当向 ArrayList 中添加一个新元素时,ArrayList 会先检查当前数组中是否还有剩余的空间可以存储元素;
- 如果有剩余空间,则将元素添加到数组 elementData 的尾部,然后更新数组 elementData 中的元素数量 size 即可。
- 如果没有剩余空间,则底层通过 ArrayList 扩容的核心方法
grow(int minCapacity)
来进行扩容,具体步骤如下:- 将数组 elementData 的长度设置为旧容量 oldCapacity;
- 然后取 oldCapacity 的大约 1.5 倍作为新容量 newCapacity,具体的计算方法是
newCapacity = oldCapacity + (oldCapacity << 1)
,这里通过使用移位运算符来加快计算速度; - 通过
Arrays.copyOf()
方法来创建一个长度为 newCapacity 的数组,并且将旧数组中的元素复制到新数组中,复制完成后,ArrayList 就会开始使用新数组来存储元素,并更新数组的大小和元素数量。
在 Java 8 之前,ArrayList 的扩容是通过
System.arraycopy()
方法来实现的,而在 Java 8 之后,则通过Arrays.copyOf()
方法来实现的。有关这两者的区别可以参考 Java 基础——System.arraycopy() 与 Arrays.copyOf() 的联系与区别这篇文章。
3.扩容测试
class Test {
//通过反射获取 list 的容量,即 elementData 数组的长度
public static Integer getCapacity(ArrayList<Integer> list) {
Integer length = null;
Class clazz = list.getClass();
Field field;
try {
field = clazz.getDeclaredField("elementData");
field.setAccessible(true);
Object[] object = (Object[]) field.get(list);
length = object.length;
return length;
} catch (Exception e) {
e.printStackTrace();
}
return length;
}
public static void main(String[] args) {
ArrayList<Integer> list = new ArrayList<>();
//记录 list 的容量,初始值为 0
int preCap = 0;
for (int i = 0; i < 100; i++) {
list.add(i);
int curCap = getCapacity(list);
// curCap > preCap 说明上次添加元素过程中发生了扩容
if (curCap > preCap) {
System.out.println("capacity: " + curCap + ", size: " + list.size());
preCap = getCapacity(list);
}
}
System.out.println("添加 100 个元素后,capacity: " + getCapacity(list) + ", size: " + list.size());
}
}
输出结果如下:
capacity: 10, size: 1
capacity: 15, size: 11
capacity: 22, size: 16
capacity: 33, size: 23
capacity: 49, size: 34
capacity: 73, size: 50
capacity: 109, size: 74
添加 100 个元素后,capacity: 109, size: 100
4.思考:为什么按大约 1.5 倍来扩容?
(1)扩容因子的大小选择,需要考虑如下情况:
- 扩容容量不能太小,防止频繁扩容,频繁申请内存空间 + 数组频繁复制;
- 扩容容量不能太大,需要充分利用空间,避免浪费过多空间;
(2)为了能充分使用之前分配的内存空间,最好把增长因子设为 1< k < 2,当 k = 1.5 时,就能充分利用前面已经释放的空间。如果 k >= 2,新容量刚刚好永远大于过去所有废弃的数组容量。除此之外,并且充分利用移位操作(右移一位,在不溢出的情况下相当于除以 2),减少了浮点数运算,提高了效率。