在Java中,ArrayList是基于动态数组实现的集合类,它的容量并不是固定不变的,会根据元素添加的情况自动进行扩容。很多开发者在使用ArrayList时,对它的默认容量和扩容机制存在疑问,下面我们就来详细分析这两个核心问题。

ArrayList的默认容量
首先需要明确的是,ArrayList在创建时如果没有指定初始容量,它的默认容量并不是在实例化对象的时候就直接分配好的。我们来看ArrayList的无参构造方法的源码:
// ArrayList无参构造方法
public ArrayList() {
// DEFAULTCAPACITY_EMPTY_ELEMENTDATA是一个空数组常量
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
从源码可以看到,无参构造创建的ArrayList底层数组是一个空数组,并没有直接分配默认容量。那默认容量10是什么时候生效的呢?是在第一次调用add方法添加元素的时候。当第一次添加元素时,ArrayList会判断当前底层数组是否为空,如果是空数组,就会将容量初始化为10,这个10就是大家常说的ArrayList默认容量。
ArrayList的扩容机制
ArrayList的扩容触发时机是当当前元素数量超过底层数组的容量时,就会执行扩容操作。我们结合add方法的源码来看整个流程:
// add方法核心逻辑
public boolean add(E e) {
// 检查是否需要扩容,size是当前元素数量
ensureCapacityInternal(size + 1);
// 将元素放到数组末尾
elementData[size++] = e;
return true;
}
private void ensureCapacityInternal(int minCapacity) {
// 如果底层数组是默认的空数组,就取默认容量10和minCapacity的较大值
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// 如果需要的容量大于当前数组长度,就执行扩容
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
核心的扩容逻辑在grow方法中,我们来看它的实现:
private void grow(int minCapacity) {
int oldCapacity = elementData.length;
// 新容量是旧容量的1.5倍,右移一位相当于除以2
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 如果新容量还是小于需要的最小容量,就用最小容量
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
// 如果新容量超过最大数组容量,就调整到最大容量
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// 拷贝原数组元素到新容量的数组中
elementData = Arrays.copyOf(elementData, newCapacity);
}
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0)
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE;
}
从上面的源码可以总结出ArrayList扩容的核心规则:
- 扩容时新容量是旧容量的1.5倍,通过
oldCapacity + (oldCapacity >> 1)实现 - 如果1.5倍扩容后的容量还是无法满足需求,就直接使用当前需要的最小容量作为新容量
- 扩容的本质是创建一个新容量的数组,然后把原数组的元素拷贝到新数组中,这个拷贝操作是有性能开销的
实际使用建议
了解了默认容量和扩容机制之后,我们在实际开发中可以注意以下几点来优化性能:
- 如果可以预估ArrayList需要存储的元素数量,尽量在创建时指定初始容量,避免多次扩容带来的数组拷贝开销。比如预计要存100个元素,就使用
new ArrayList<>(100)来初始化 - ArrayList的扩容是1.5倍增长,这个增长因子是比较合理的,既不会增长太慢导致频繁扩容,也不会增长太快浪费内存
- 如果需要频繁在ArrayList中间插入或删除元素,ArrayList的性能不如LinkedList,因为数组的插入删除需要移动后续元素,这种情况可以考虑使用LinkedList
常见问题说明
有开发者会问,为什么无参构造的时候不直接分配10的容量,而是要等到第一次add的时候才分配?这是因为延迟初始化的设计,如果创建了ArrayList之后一直没有添加元素,就不会分配10个容量的数组空间,能节省内存资源。
还有开发者会疑惑,指定初始容量之后,ArrayList就一定会按照这个容量来吗?不是的,如果添加的元素数量超过了指定的初始容量,依然会触发扩容机制,初始容量只是第一次分配数组的大小,后续还是会根据规则动态扩容。