ArrayList 与 LinkedList 对比与源码解读
目录
- 1. 基本概述与对比
- 2. ArrayList详解
- 3. LinkedList详解
- 4. 性能对比分析
- 5. 最佳实践
1. 基本概述与对比
1.1 整体对比
特性 | ArrayList | LinkedList |
---|---|---|
底层实现 | 动态数组 | 双向链表 |
随机访问 | O(1) | O(n) |
插入删除 | O(n) | O(1) |
内存占用 | 连续内存空间 | 零散内存空间 |
默认容量 | 10 | 无初始容量 |
1.2 数据结构示意图
ArrayList结构:
┌─────┬─────┬─────┬─────┬─────┐
│ 0 │ 1 │ 2 │ 3 │ 4 │
└─────┴─────┴─────┴─────┴─────┘
LinkedList结构:
┌───┐ ┌───┐ ┌───┐
│ 1 │<-->│ 2 │<-->│ 3 │
└───┘ └───┘ └───┘
2. ArrayList详解
2.1 核心属性
public class ArrayList<E> extends AbstractList<E> {
// 默认初始容量
private static final int DEFAULT_CAPACITY = 10;
// 存储元素的数组
transient Object[] elementData;
// 实际元素数量
private int size;
// 用于空实例的共享空数组
private static final Object[] EMPTY_ELEMENTDATA = {};
// 用于默认大小的空实例的共享数组
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
}
2.2 动态扩容机制
ArrayList最关键的特性是动态扩容。当数组空间不足时,会创建更大的数组并复制元素。
private void grow(int minCapacity) {
// 获取旧容量
int oldCapacity = elementData.length;
// 新容量 = 旧容量 + 旧容量/2 (增加50%)
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);
}
扩容过程示意图:
原数组: [1,2,3,4,5] (容量5)
↓ 添加元素触发扩容
新数组: [1,2,3,4,5,_,_] (容量7)
2.3 核心方法实现
2.3.1 添加元素
public boolean add(E e) {
// 确保容量足够
ensureCapacityInternal(size + 1);
// 将元素添加到数组末尾
elementData[size++] = e;
return true;
}
public void add(int index, E element) {
// 检查索引是否合法
rangeCheckForAdd(index);
// 确保容量足够
ensureCapacityInternal(size + 1);
// 将index后的元素向后移动一位
System.arraycopy(elementData, index,
elementData, index + 1,
size - index);
// 在index位置插入新元素
elementData[index] = element;
size++;
}
2.3.2 删除元素
public E remove(int index) {
// 检查索引是否合法
rangeCheck(index);
modCount++;
// 获取要删除的元素
E oldValue = elementData(index);
// 计算要移动的元素个数
int numMoved = size - index - 1;
if (numMoved > 0)
// 将后面的元素向前移动一位
System.arraycopy(elementData, index+1,
elementData, index,
numMoved);
// 清除最后一个元素的引用
elementData[--size] = null;
return oldValue;
}
3. LinkedList详解
3.1 核心属性和节点结构
public class LinkedList<E> {
// 链表大小
transient int size = 0;
// 头节点
transient Node<E> first;
// 尾节点
transient Node<E> last;
// 节点类定义
private static class Node<E> {
E item; // 元素值
Node<E> next; // 后继节点
Node<E> prev; // 前驱节点
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
}
3.2 核心方法实现
3.2.1 添加元素
public boolean add(E e) {
// 在链表末尾添加节点
linkLast(e);
return true;
}
void linkLast(E e) {
// 保存原尾节点
final Node<E> l = last;
// 创建新节点
final Node<E> newNode = new Node<>(l, e, null);
// 更新尾节点
last = newNode;
// 如果原尾节点为空,说明是空链表
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}
添加过程示意图:
原链表: A <-> B
新元素: C
结果: A <-> B <-> C
3.2.2 删除元素
public E remove(int index) {
// 检查索引是否合法
checkElementIndex(index);
// 删除并返回指定位置的节点
return unlink(node(index));
}
E unlink(Node<E> x) {
final E element = x.item;
final Node<E> next = x.next;
final Node<E> prev = x.prev;
// 处理前驱节点
if (prev == null) {
first = next;
} else {
prev.next = next;
x.prev = null;
}
// 处理后继节点
if (next == null) {
last = prev;
} else {
next.prev = prev;
x.next = null;
}
x.item = null;
size--;
modCount++;
return element;
}
4. 性能对比分析
4.1 时间复杂度对比
操作 | ArrayList | LinkedList | 说明 |
---|---|---|---|
add(E) | O(1) | O(1) | 平均情况 |
add(int, E) | O(n) | O(n) | 需要移动元素/遍历到指定位置 |
remove(int) | O(n) | O(n) | 需要移动元素/遍历到指定位置 |
get(int) | O(1) | O(n) | 数组支持随机访问 |
contains(Object) | O(n) | O(n) | 都需要遍历 |
4.2 内存占用对比
ArrayList:
- 需要连续的内存空间
- 扩容时需要额外空间
- 每个元素占用空间小
LinkedList:
- 不需要连续内存空间
- 不需要预留空间
- 每个元素需要额外的前后指针空间
4.3 适用场景对比
ArrayList适用于:
- 频繁随机访问的场景
- 元素数量相对固定的场景
- 对内存空间要求高的场景
LinkedList适用于:
- 频繁在任意位置插入、删除的场景
- 元素数量变化较大的场景
- 不需要随机访问的场景
5. 最佳实践
5.1 ArrayList最佳实践
- 初始容量设置
// 如果知道大概容量,指定初始值避免扩容
ArrayList<String> list = new ArrayList<>(10000);
- 批量操作优化
// 使用ensureCapacity预分配空间
list.ensureCapacity(10000);
for(int i = 0; i < 10000; i++) {
list.add(i);
}
- 删除元素优化
// 从后向前删除,避免元素移动
for(int i = list.size() - 1; i >= 0; i--) {
if(condition) {
list.remove(i);
}
}
5.2 LinkedList最佳实践
- 优化遍历操作
// 使用迭代器遍历
Iterator<String> iter = list.iterator();
while(iter.hasNext()) {
String item = iter.next();
if(condition) {
iter.remove(); // 使用迭代器的remove方法
}
}
- 合理使用双向特性
// 如果要访问的位置靠近尾部,可以从尾部开始遍历
if(index > (size >> 1)) {
Node<E> x = last;
for(int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
5.3 选择建议
- 根据实际场景选择合适的List实现:
- 如果不确定,优先使用ArrayList
- 如果频繁插入删除,考虑LinkedList
- 如果需要随机访问,必须使用ArrayList
- 注意避免的做法:
- 不要在ArrayList中间频繁插入删除
- 不要在LinkedList中频繁随机访问
- 不要频繁扩容ArrayList
总结
- ArrayList和LinkedList各有优势:
- ArrayList: 随机访问快,内存占用少
- LinkedList: 插入删除快,空间利用灵活
- 核心区别:
- 底层数据结构不同
- 操作效率侧重点不同
- 内存占用模式不同
- 使用建议:
- 根据实际场景选择合适的实现
- 关注性能关键点
- 遵循最佳实践
通过深入理解这两种List的实现原理和特点,可以在实际开发中做出更好的选择,写出更高效的代码。