一、复杂度 × 内存 × 扩容总表(n 为元素个数)
| 容器 | get(i)/set(i) | 末尾 add/remove | 头部 add/remove | 中间插入/删除(已知索引) | contains/遍历 | 额外内存开销 | 扩容/收缩策略 | 典型场景 |
|---|---|---|---|---|---|---|---|---|
| ArrayList | O(1) | 摊还 O(1) (偶发 O(n)) | O(n) | O(n)(搬移) | O(n) | 低:一个 Object[] + 少量空槽 | 按比例扩容(约 1.5×) ;默认不自动收缩,可 trimToSize() | 绝大多数业务列表、排序/二分、随机访问 |
| LinkedList | O(n) | O(1)(有尾指针) | O(1) | 定位 O(n),定位后 O(1) | O(n) | 高:每元素一 Node(prev/next 指针 + 对象头) | 无“扩容”,每次插入分配新 Node;释放即内存可回收 | 仅当已持有节点/迭代器且频繁插删 |
| ArrayDeque | 不支持下标随机访问 | O(1) | O(1) | 不支持中间插入 | 遍历 O(n) | 低:环形 Object[] | 容量 2 的幂,满时 ×2;通常不自动收缩 | 队列/栈/双端队列(优于 LinkedList) |
| CopyOnWriteArrayList | O(1) | O(n) (写时复制) | O(n) | O(n) | 读遍历 O(n)(无锁),迭代弱一致 | 中:底层数组 + 写时复制的暂态数组 | 每次写产生新数组(约 1.5× 拷贝) ,无自动收缩 | 读多写少、监听器列表、事件分发 |
速记:随机访问/遍历 → ArrayList;双端队列 → ArrayDeque;已定位节点附近频繁插删才考虑 LinkedList;读多写少的观察者列表 → CopyOnWriteArrayList。****
二、逐个讲清
1) ArrayList(动态数组)
-
内存模型:一段连续的 Object[],size ≤ capacity,未用槽位是 null 引用;引用大小与平台相关(常见 4B/8B)。
-
扩容:不足时 按比例扩容(常见 ≈1.5×) → Arrays.copyOf 整体搬移到更大的数组;因此尾插是摊还 O(1) 。
-
收缩:默认不主动收缩(避免抖动与频繁拷贝),可在需要时 trimToSize() 或重新构造。
-
性能特征:
- CPU 缓存友好(连续内存),遍历/随机访问很快;
- 中间插删要搬移,O(n) ;大量删除时建议倒序或 removeIf。
-
实战建议:批量插入前 ensureCapacity(expected) 降低多次扩容成本。
2) LinkedList(双向链表)
-
内存模型:每个元素对应一个 Node{prev, item, next} 对象,不连续;每个节点多出 2 个指针与对象头,常数开销大,GC 更频繁。
-
复杂度:已定位到节点后插删 O(1) ,但定位本身 O(n) ;get(i) 需要从近端线性走。
-
适用面:仅在已持有迭代器/节点且在其附近高频插删、或头尾操作很多的少数场景。
-
对比:做队列/栈,ArrayDeque 往往更快更省内存。
3) ArrayDeque(环形数组的双端队列)
-
内存模型:一个容量为 2 的幂 的 Object[],用 head/tail 下标环形前进(按位与掩码取模),空槽为 null。
-
扩容/收缩:满时容量 ×2 并重排;通常不自动收缩(避免震荡),可在“长期变小”的场景手动重建。
-
性能特征:头/尾入出都是 O(1) ;没有中间插入语义,也没有下标随机访问。
-
选型:队列/栈/双端队列首选,绝大多数情况下优于 LinkedList。
4) CopyOnWriteArrayList(写时复制数组)
- 内存模型:内部是数组;每次写都会复制出一个新数组并替换旧引用,读侧看到的是快照。
- 复杂度:读遍历无锁但写是 O(n) ,且会产生短暂两份数组的内存占用。
- 适用面:读远多于写(例如监听器/订阅者列表、系统配置快照);写入频繁或列表很大时不合适。
- 坑点:迭代器是快照语义,读时看不到并发写入(符合其设计)。
三、内存成本的直觉刻度(64 位 + 压缩 OOP 场景的粗量级)
只给相对结论,避免平台细节差异误导:
- ArrayList/ArrayDeque:一块数组 + 若干空槽引用;每元素仅 1 个引用位(外加数组对象头)。
- LinkedList:每元素额外 2 个引用 + 1 个节点对象头,远高于动态数组;局部性差,CPU 预取命中低。
- CopyOnWriteArrayList:与 ArrayList 类似,但写入瞬间会出现“一旧一新两块数组”的临时双倍占用。
四、扩容与“摊还 O(1)”的直观解释
- 动态数组类(ArrayList/ArrayDeque)在空间满时做几何扩张(如 1.5× / 2×),虽然某次扩容是 O(n) 拷贝,但把成本摊到每次尾插上,平均下来仍是 O(1) 。
- 若能提前知道规模,ensureCapacity(或预估初始容量)可以显著减少扩容次数与总拷贝量。
- 为什么不自动收缩? 收缩同样要 O(n) 拷贝,还可能马上又扩回去,导致“抖动”。因此主流实现默认不收缩,交给业务在“规模长期下降”时手动 trim/rebuild。
五、选型清单(实践版)
- 绝大多数列表:ArrayList / Kotlin MutableList(底层就是 ArrayList)。
- 队列/栈/双端:ArrayDeque(比 LinkedList 更好)。
- 读多写少、监听器:CopyOnWriteArrayList。
- 只有在“已定位节点 + 高频插删” :LinkedList 才有优势。
- Android 额外提示:不要用 LinkedList 做 RecyclerView 数据源;大数据量时 ArrayList 的局部性胜出明显。
六、两段常用代码模板
ArrayList 提前扩容
var list = new ArrayList<Foo>(/* expectedSize */ 10_000);
// 批量 add 期间避免多次扩容
ArrayDeque 作为队列/栈
Deque<Task> q = new ArrayDeque<>();
q.offer(task); // 入队 O(1)
Task t = q.poll(); // 出队 O(1)
ArrayDeque<String> stack = new ArrayDeque<>();
stack.push("a"); // 压栈 O(1)
stack.pop(); // 弹栈 O(1)
一句话总结
默认 ArrayList,队列用 ArrayDeque;读多写少用 CopyOnWriteArrayList;只有“已定位节点 + 高频插删”再考虑 LinkedList。****
在需要高性能时,预估容量、避免中部操作、用连续内存争取缓存友好,是最划算的三件事。