在 Java 中,List
是一种常见的集合类型,特别是 ArrayList
和 LinkedList
是两个常用的实现类。由于 ArrayList
是基于动态数组实现的,因此它会在需要时自动进行扩容,以便容纳更多的元素。在 ArrayList
中,扩容是一个非常关键的机制,它能够保证 ArrayList
在不断添加元素时保持良好的性能。
1. 为什么需要扩容?
ArrayList
基于数组实现,而数组的大小是固定的。因此,ArrayList
在容量满时需要进行扩容。当我们向 ArrayList
添加元素时,如果当前的数组已经没有空间容纳新元素,就会触发扩容操作。扩容的目的是:
- 保证
ArrayList
能够容纳更多的元素。 - 避免因为容量不足而频繁地增加数组大小,从而提高性能。
2. 扩容的触发条件
ArrayList
的扩容是根据其当前的 容量(capacity
)和 大小(size
)来决定的。容量表示当前数组可以存储的元素个数,大小表示已经存储的元素个数。扩容的触发条件是当 当前元素个数(size
)达到 容量(capacity
)时。
默认情况下:
ArrayList
会根据需要动态调整容量。- 初始容量是 10。
- 默认的扩容策略是当容量满时,容量会扩展到 原来容量的 1.5 倍。
3. 扩容的过程
当 ArrayList
的容量不够时,会自动扩容。以下是扩容的具体过程:
3.1. 创建一个新数组
ArrayList
会创建一个新的数组,这个数组的大小通常是原数组大小的 1.5 倍。假设原始数组的容量是 n
,扩容后新数组的容量是 n + n/2
,即 n
的 1.5 倍(具体实现可能因 JDK 版本有所不同)。
3.2. 将原有元素复制到新数组
扩容后,ArrayList
会将原数组中的所有元素复制到新的数组中。此过程的时间复杂度是 O(n),其中 n 是当前 ArrayList
的元素个数。
3.3. 更新数组引用
扩容完成后,ArrayList
会更新内部数组的引用,使其指向新创建的数组,并继续使用这个新数组来存储新的元素。
4. 扩容的代码分析(以 JDK 8 为例)
在 JDK 8 中,ArrayList
的扩容过程通过 ensureCapacity
方法来实现:
private void grow(int minCapacity) {
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1); // 容量扩展为原来容量的1.5倍
if (newCapacity - minCapacity < 0) {
newCapacity = minCapacity; // 如果新的容量还是不足,直接设置为所需容量
}
elementData = Arrays.copyOf(elementData, newCapacity); // 拷贝到新的数组
}
elementData
是ArrayList
用来存储元素的数组。oldCapacity
是当前数组的容量。newCapacity
是扩容后的新容量,计算方式是原来容量的 1.5 倍。Arrays.copyOf
是用来创建一个新的数组并将旧数组的元素复制到新数组中的方法。
5. 扩容的影响
扩容是 ArrayList
中常见的一种操作,虽然它确保了 ArrayList
的灵活性,但也有一定的开销。以下是扩容的影响:
5.1. 性能开销
- 扩容时,
ArrayList
需要创建一个新数组,并将旧数组的元素复制到新数组中,这个过程的时间复杂度是 O(n),其中 n 是当前ArrayList
的大小。 - 频繁扩容会影响性能,因为每次扩容都需要复制整个数组。
5.2. 内存开销
- 扩容时,
ArrayList
会分配一个新的更大的数组,旧数组会被垃圾回收器回收。如果扩容发生频繁,会导致内存使用效率降低。 - 尽管扩容时会额外分配一些内存(通常是原容量的 1.5 倍),但也可以有效避免频繁的内存分配操作。
6. 如何优化扩容?
6.1. 合理设置初始容量
如果已知将要存储的元素数量,可以在创建 ArrayList
时指定一个合适的初始容量,以避免多次扩容。例如,如果预计将存储 1000 个元素,应该将初始容量设置为 1000,这样就可以避免扩容。
List<Integer> list = new ArrayList<>(1000); // 预设初始容量为1000
6.2. 使用 ensureCapacity
提前扩容
如果你知道未来将会插入大量元素,可以通过 ensureCapacity
方法提前扩容,这样可以避免在插入时频繁扩容。
list.ensureCapacity(1000); // 确保容量至少为 1000
6.3. 避免不必要的插入操作
每次 ArrayList
扩容都会带来一定的性能成本。如果可能,应该避免频繁插入元素,尤其是在大量数据的场景下。
6.4. 使用其他容器
如果对内存和性能有特殊要求,也可以考虑使用其他 List
实现(例如 LinkedList
)或其他集合类型(如 CopyOnWriteArrayList
、Vector
),这些实现的扩容机制和性能特点有所不同。
7. 扩容示例
以下是一个简单的示例,演示了 ArrayList
如何自动扩容:
import java.util.ArrayList;
public class ArrayListResizeExample {
public static void main(String[] args) {
// 创建一个容量为 2 的 ArrayList
ArrayList<Integer> list = new ArrayList<>(2);
// 添加一些元素,触发扩容
list.add(1);
list.add(2);
// 扩容会发生,因为当前容量已经满了
list.add(3);
System.out.println("ArrayList: " + list);
}
}
在上述代码中,ArrayList
初始容量为 2,添加 3 个元素后会触发扩容。此时容量会变为 3 或更大(具体值取决于 JDK 版本),并且元素会被重新分配到新数组中。
8. 总结
ArrayList
是一个动态数组实现,默认容量为 10。- 扩容发生在元素个数超过当前容量时,容量会增长为原容量的 1.5 倍。
- 扩容时,
ArrayList
会创建一个新的数组并将旧数组中的元素复制到新数组中,时间复杂度是 O(n)。 - 频繁的扩容会带来性能和内存开销,合理设置初始容量和使用
ensureCapacity
可以优化扩容过程。
通过了解 ArrayList
的扩容机制,我们可以更有效地使用 List
,提高代码的性能和内存效率。