
一、底层数据结构与特性
1. 核心数据结构
// JDK 17 源码片段
// 默认初始容量
private static final int DEFAULT_CAPACITY = 10;
// 存储元素的数组缓冲区
private static final Object[] EMPTY_ELEMENTDATA = {};
// 当前元素数量
private int size;
2. 关键特性
特性 | 说明 |
---|---|
动态扩容 | 初始容量10,扩容系数1.5 (oldCapacity + (oldCapacity >> 1)) |
快速随机访问 | 实现 RandomAccess 接口,get(index) 时间复杂度 O(1) |
非线程安全 | 多线程环境下需要外部同步 |
允许 null 值 | 可以存储任意数量的 null |
Fail-Fast 迭代器 | 迭代过程中检测到结构性修改会抛出 ConcurrentModificationException |
二、核心机制深度解析
1. 扩容机制源码分析
// 将指定的元素附加到此列表的末尾。
public boolean add(E e) {
modCount++;
add(e, elementData, size);
return true;
}
private void add(E e, Object[] elementData, int s) {
if (s == elementData.length) // 容量已满
elementData = grow(); // 触发扩容
elementData[s] = e;
size = s + 1;
}
private Object[] grow() {
return grow(size + 1); // 最小需求容量 = 当前size + 1
}
// 增加容量以确保它至少可以容纳最小容量参数指定的元素数。
private Object[] grow(int minCapacity) {
int oldCapacity = elementData.length;
// 如果当前容量大于0或使用的不是默认空数组,则计算新容量并复制元素
if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
int newCapacity = ArraysSupport.newLength(
oldCapacity,
minCapacity - oldCapacity, // 最小增量
oldCapacity >> 1 // 首选增量(50%)
);
return elementData = Arrays.copyOf(elementData, newCapacity);
} else {
// 否则创建一个容量为默认值(10)与最小需求容量之间较大值的新数组。
return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)];
}
}
2. 快速随机访问
// 返回此列表中指定位置的元素。
public E get(int index) {
// 校验索引是否存在
Objects.checkIndex(index, size);
return elementData(index);
}
// 如果索引越界,则根据提供的异常格式化器抛出相应运行时异常;否则返回原索引。
@IntrinsicCandidate
public static <X extends RuntimeException>
int checkIndex(int index, int length,
BiFunction<String, List<Number>, X> oobef) {
if (index < 0 || index >= length)
throw outOfBoundsCheckIndex(oobef, index, length);
return index;
}
// Positional Access Operations
@SuppressWarnings("unchecked")
E elementData(int index) {
return (E) elementData[index];
}
3. 迭代器 Fail-Fast 原理
// 迭代器迭代时,检测到结构性修改会抛出 ConcurrentModificationException
private class Itr implements Iterator<E> {
int cursor; // 下一个元素的索引
int lastRet = -1; // 最后返回元素的索引
int expectedModCount = modCount; // 创建迭代器时的修改计数
public E next() {
checkForComodification(); // 检查是否被修改
// ... 获取元素逻辑
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
// 判断当前ArrayList是否与指定对象相等
public boolean equals(Object o) {
if (o == this) {
return true;
}
if (!(o instanceof List)) {
return false;
}
final int expectedModCount = modCount;
// ArrayList can be subclassed and given arbitrary behavior, but we can
// still deal with the common case where o is ArrayList precisely
boolean equal = (o.getClass() == ArrayList.class)
? equalsArrayList((ArrayList<?>) o)
: equalsRange((List<?>) o, 0, size);
// 检查是否有并发修改,确保比较过程的一致性。
checkForComodification(expectedModCount);
return equal;
}
三、性能关键点分析
1. 时间复杂度对比
操作 | 时间复杂度 | 说明 |
---|---|---|
get(int index) | O(1) | 直接数组索引访问 |
add(E element) | 均摊 O(1) | 尾部添加,偶尔触发 O(n) 扩容 |
add(int index, E) | O(n) | 需要移动 index 后的所有元素 |
remove(int index) | O(n) | 需要移动 index 后的所有元素 |
contains(Object) | O(n) | 需要遍历整个列表 |
Iterator.remove() | O(n) | 同 remove(int index) |
2. 空间优化技巧
// 添加元素后释放多余空间
public void trimToSize() {
modCount++;
if (size < elementData.length) {
elementData = (size == 0)
? EMPTY_ELEMENTDATA
: Arrays.copyOf(elementData, size);
}
}
// 预设容量避免频繁扩容
List<String> list = new ArrayList<>(10000); // 初始化指定容量
四、线程安全解决方案
1. 同步包装器
// 将普通ArrayList包装成线程安全的同步列表
List<String> syncList = Collections.synchronizedList(new ArrayList<>());
// 适用于多线程环境下需要安全访问列表元素的场景,但需注意遍历时仍需手动加锁
synchronized (syncList) {
Iterator<String> it = syncList.iterator();
while (it.hasNext()) {
System.out.println(it.next());
}
}
2. CopyOnWriteArrayList
List<String> safeList = new CopyOnWriteArrayList<>();
// 读操作无需加锁
for (String s : safeList) {
System.out.println(s);
}
// 写操作线程安全(但性能较低)
safeList.add("new element");
3. 方案对比
方案 | 适用场景 | 优点 | 缺点 |
---|---|---|---|
Collections.synchronizedList | 中等写操作频率 | 实现简单,通用性强 | 读写均需同步,性能中等 |
CopyOnWriteArrayList | 读多写极少(配置管理) | 读操作完全无锁,性能极高 | 写性能差,内存占用高 |
五、经典问题解析
1. ArrayList 和 LinkedList 如何选择?
特性 | ArrayList | LinkedList |
---|---|---|
底层结构 | 动态数组 | 双向链表 |
随机访问 | O(1) | O(n) |
头部插入 | O(n)(需要移动元素) | O(1) |
尾部插入 | O(1)(均摊) | O(1) |
内存占用 | 较少(仅需存储数据) | 较高(需要存储前后指针) |
缓存友好 | 是(空间局部性) | 否 |
选择标准:
- 随机访问频率高 → ArrayList(O(1) vs O(n))
- 频繁在任意位置插入删除 → LinkedList(O(1) vs O(n))
- 内存敏感 → ArrayList(更少内存开销)
- 大数据量遍历 → 两者迭代器性能接近
2. 如何避免 ConcurrentModificationException?
// 错误示例(会抛异常)
for (String s : list) {
if (s.equals("remove")) {
list.remove(s); // 结构性修改
}
}
// 正确方案1:使用迭代器的remove方法
// 该方法会同步更新迭代器内部的 expectedModCount 和集合的 modCount
Iterator<String> it = list.iterator();
while (it.hasNext()) {
String s = it.next();
if (s.equals("remove")) {
it.remove(); // 安全删除
}
}
// 正确方案2:使用Java8+ removeIf
list.removeIf(s -> s.equals("remove"));
3. subList 的陷阱
ArrayList<Integer> mainList = new ArrayList<>(Arrays.asList(1,2,3,4,5));
// 返回的是原列表的一个视图,不是独立的副本
List<Integer> sub = mainList.subList(1, 3); // [2,3]
// 修改子视图会影响原列表
sub.set(0, 99);
System.out.println(mainList); // [1, 99, 3, 4, 5]
// 原列表结构修改会导致子视图操作异常
mainList.add(6);
sub.get(0); // 抛出 ConcurrentModificationException
// 正确用法:创建独立副本
List<Integer> safeSub = new ArrayList<>(mainList.subList(1, 3));
六、性能优化实践
1. 预分配容量
// 已知数据量时的最佳实践
int expectedSize = 100000;
List<String> list = new ArrayList<>(expectedSize);
// 避免扩容操作
for (int i = 0; i < expectedSize; i++) {
list.add("item-" + i); // 无扩容开销
}
2. 高效元素初始化
// 使用 Arrays.asList 快速初始化(但返回的是固定大小列表)
List<String> fixedList = Arrays.asList("A", "B", "C");
// 创建可修改的ArrayList
List<String> modifiable = new ArrayList<>(Arrays.asList("A", "B", "C"));
// Java 9+ 工厂方法
List<String> list = List.of("A", "B", "C"); // 不可变
List<String> arrayList = new ArrayList<>(List.of("A", "B", "C"));
3. 高效批量操作
// 批量删除(避免多次移动元素)
public void removeRange(int fromIndex, int toIndex) {
modCount++;
int numMoved = size - toIndex;
System.arraycopy(elementData, toIndex, elementData, fromIndex, numMoved);
int newSize = size - (toIndex - fromIndex);
Arrays.fill(elementData, newSize, size, null); // 清空引用
size = newSize;
}
// 使用示例(删除索引2-4的元素)
list.subList(2, 5).clear();
七、常见陷阱与解决方案
1. 多线程并发修改
场景:
// 线程1
for (String s : list) { ... }
// 线程2
list.add("new");
解决方案:
- 使用
CopyOnWriteArrayList
- 外部同步所有访问
- 使用并发集合如
ConcurrentLinkedQueue
2. 存储大对象导致内存碎片
优化方案:高效存储大量固定大小的数据对象,堆外内存(直接缓冲区) 来优化内存使用和I/O性能
// 使用直接缓冲区存储
public class LargeObjectList {
// 直接字节缓冲区(堆外内存)不占用JVM堆空间,避免大对象触发Full GC
private ByteBuffer buffer;
// 每个数据元素的固定字节大小
private int elementSize;
public LargeObjectList(int capacity, int elementSize) {
this.elementSize = elementSize;
// 分配直接缓冲区(堆外内存)
buffer = ByteBuffer.allocateDirect(capacity * elementSize);
}
public void add(byte[] data) {
// 严格大小校验
if (data.length != elementSize)
throw new IllegalArgumentException();
// 数据直接写入缓冲区
buffer.put(data);
}
}
3. 高频读取 + 低频写入(如:实时监控数据采集)
方案:在满足线程安全的前提下,显著提升了读操作的并发性能
// 使用双缓冲技术
public class DoubleBufferedList<E> {
// 写缓冲区,所有新增元素写入此列表(需加锁)
private ArrayList<E> writeBuffer = new ArrayList<>();
// 读缓冲区,提供数据的只读视图(volatile保证可见性)
private volatile ArrayList<E> readBuffer = new ArrayList<>();
public void add(E e) {
synchronized (this) {
// 元素写入写缓冲区
writeBuffer.add(e);
}
}
// 获取数据快照(线程安全)
public List<E> snapshot() {
synchronized (this) {
if (!writeBuffer.isEmpty()) {
// 将写缓冲区的数据复制到读缓冲区
readBuffer = new ArrayList<>(writeBuffer);
writeBuffer.clear();
}
// 返回读缓冲区的只读视图
return Collections.unmodifiableList(readBuffer);
}
}
}
注意事项:
- 在已知元素数量时务必预设容量,避免扩容开销
- 多线程环境优先考虑 CopyOnWriteArrayList 或 ConcurrentLinkedQueue
- 超大数据集考虑使用 分页加载 或 数据库支持 的解决方案
- 关注 内存局部性原理,连续内存访问比链表有显著性能优势