ArrayList
和 LinkedList
是 Java 集合框架中两种常用的列表实现,它们在底层数据结构、性能特点和适用场景上有显著的区别。以下是它们的详细对比以及 ArrayList
的扩容机制。
1. ArrayList 和 LinkedList 的底层区别
(1) 底层数据结构
ArrayList
:- 基于动态数组(Dynamic Array)实现。
- 元素在内存中是连续存储的。
- 支持随机访问,可以通过索引快速定位元素。
LinkedList
:- 基于双向链表(Doubly Linked List)实现。
- 每个元素(节点)包含数据和两个指针,分别指向前后节点。
- 不支持随机访问,必须从头或尾遍历链表才能访问指定位置的元素。
(2) 性能特点
特性 | ArrayList |
LinkedList |
---|---|---|
随机访问 | 快速(时间复杂度为 O(1))。 | 慢(时间复杂度为 O(n),需要遍历链表)。 |
插入/删除 | 较慢(时间复杂度为 O(n),可能需要移动元素)。 | 快速(时间复杂度为 O(1),只需调整指针)。 |
内存占用 | 较低(仅存储元素和少量额外信息)。 | 较高(每个节点需要存储数据和两个指针)。 |
(3) 使用场景
ArrayList
:- 适用于频繁随机访问元素的场景。
- 适合读多写少的场景。
- 示例:存储一组用户 ID,并根据索引快速查找某个用户。
LinkedList
:- 适用于频繁插入和删除操作的场景。
- 适合写多读少的场景。
- 示例:实现队列或栈等数据结构。
2. ArrayList 的扩容机制
(1) 动态数组的特点
ArrayList
内部使用一个数组来存储元素。- 当数组容量不足时,
ArrayList
会自动扩容以容纳更多元素。
(2) 扩容过程
初始容量:
- 默认初始容量为 10。
- 可以通过构造方法指定初始容量。
ArrayList<Integer> list = new ArrayList<>(20); // 初始容量为 20
扩容触发条件:
- 当添加新元素时,如果当前数组容量不足以容纳新元素,则触发扩容。
扩容策略:
- 新容量通常是原容量的 1.5 倍(即原容量 + 原容量的一半)。
- 扩容后,会创建一个新的数组,并将原数组中的所有元素复制到新数组中。
- 源码示例(JDK 8 中的部分代码):
private void grow(int minCapacity) { int oldCapacity = elementData.length; int newCapacity = oldCapacity + (oldCapacity >> 1); // 扩容为原来的 1.5 倍 if (newCapacity - minCapacity < 0) newCapacity = minCapacity; if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); elementData = Arrays.copyOf(elementData, newCapacity); // 复制到新数组 }
性能影响:
- 扩容操作涉及数组的复制,因此是一个耗时的操作。
- 如果可以预估元素数量,建议在初始化时指定合适的容量,避免频繁扩容。
步骤
计算新容量:新容量通常是旧容量的 1.5 倍(即
oldCapacity + (oldCapacity >> 1)
)。例如,若当前容量为 10,则扩容后为 15。比较最小需要容量:如果计算得到的新容量仍小于所需的最小容量(
minCapacity
),则将新容量设为minCapacity
。检查最大容量限制:如果新容量超过了
ArrayList
能支持的最大容量(Integer.MAX_VALUE - 8
),则将新容量设为Integer.MAX_VALUE
。数组复制:最终,使用
Arrays.copyOf()
方法将原数组的元素复制到新数组中,并将新数组赋值给elementData
。
(3) 扩容示例
import java.util.ArrayList;
public class Main {
public static void main(String[] args) {
ArrayList<Integer> list = new ArrayList<>();
System.out.println("Initial capacity: " + getCapacity(list));
for (int i = 0; i < 15; i++) {
list.add(i);
System.out.println("After adding " + i + ", capacity: " + getCapacity(list));
}
}
// 获取 ArrayList 的当前容量(反射方式)
private static int getCapacity(ArrayList<?> list) {
try {
java.lang.reflect.Field field = ArrayList.class.getDeclaredField("elementData");
field.setAccessible(true);
return ((Object[]) field.get(list)).length;
} catch (Exception e) {
e.printStackTrace();
return -1;
}
}
}
运行结果:
Initial capacity: 10
After adding 0, capacity: 10
After adding 1, capacity: 10
...
After adding 9, capacity: 10
After adding 10, capacity: 15
After adding 11, capacity: 15
...
3. 总结
(1) ArrayList 和 LinkedList 的区别
特性 | ArrayList |
LinkedList |
---|---|---|
底层数据结构 | 动态数组 | 双向链表 |
随机访问 | 快速(O(1)) | 慢(O(n)) |
插入/删除 | 较慢(O(n)) | 快速(O(1)) |
内存占用 | 较低 | 较高 |
适用场景 | 频繁随机访问、读多写少 | 频繁插入删除、写多读少 |
(2) ArrayList 的扩容机制
- 默认初始容量为 10。
- 扩容时,新容量为原容量的 1.5 倍。
- 扩容涉及数组复制,可能会带来性能开销。
- 如果可以预估元素数量,建议在初始化时指定合适的容量。