1 minute read

ArrayList vs. LinkedList: 深入底层

对于 Java 面试来说,ArrayListLinkedList 的区别是一个经典问题。简单地回答“一个是数组,一个是链表”是远远不够的。面试官希望你能够深入到底层数据结构,并分析它们在不同场景下的性能优劣。

总之一句话:需要深入了解底层实现。


1. 底层数据结构

ArrayList

  • 内部实现ArrayList 的底层是一个动态数组,其核心是一个 Object[] 数组。
    // ArrayList 核心成员变量 (简化后)
    transient Object[] elementData;
    private int size;
    
  • 特点
    • 连续内存空间:所有元素在内存中是连续存储的。
    • 动态扩容:当添加元素导致数组容量不足时,ArrayList 会创建一个更大的新数组(通常是原容量的1.5倍),并将旧数组的元素复制到新数组中。这是一个耗时的操作。

LinkedList

  • 内部实现LinkedList 的底层是一个双向链表。它由一系列的节点 (Node) 组成。
    // LinkedList 核心成员变量 (简化后)
    transient Node<E> first;
    transient Node<E> last;
    private int size;
    
    // 内部节点类
    private static class Node<E> {
        E item;
        Node<E> next;
        Node<E> prev;
        // ... 构造函数 ...
    }
    
  • 特点
    • 非连续内存空间:每个节点都存储着元素本身,以及指向前一个节点 (prev) 和后一个节点 (next) 的引用。元素在内存中是分散的。
    • 无需扩容:添加元素时,只需创建新节点并调整相邻节点的引用即可,没有数组扩容的开销。

2. 核心操作性能对比 (Big O)

这是面试中最关键的部分。

操作 ArrayList LinkedList 原因
随机访问 (get(int index)) O(1) O(n) ArrayList 基于数组,可通过索引直接定位。LinkedList 需从头或尾开始遍历 n/2 个元素。
头部添加 (add(0, E)) O(n) O(1) ArrayList 需要将所有元素后移一位。LinkedList 只需修改头节点的引用。
尾部添加 (add(E)) O(1) (摊销) O(1) ArrayList 大部分情况是 O(1),但可能触发 O(n) 的扩容。LinkedList 只需修改尾节点的引用。
中间添加/删除 (add/remove) O(n) O(n) ArrayList 需要移动后续元素。LinkedList 虽然插入/删除本身是O(1),但首先需要 O(n) 的时间遍历到指定位置。
使用 Iterator 删除 O(n) O(1) ArrayList 移除后仍需移动元素。LinkedList 的迭代器持有对当前节点的引用,移除操作仅需修改前后节点的指针,这是 LinkedList 的核心优势场景。

3. 内存占用

  • ArrayList
    • 优点:结构简单,每个元素只占用数据本身所需的空间。
    • 缺点:可能会有部分空间被浪费(数组的 capacity 大于实际的 size)。
  • LinkedList
    • 优点:空间利用率高,按需分配。
    • 缺点内存开销更大。每个元素都需要一个额外的 Node 对象来包装,Node 对象中还包含两个引用 (prev, next)。对于存储小型数据,这部分开销相当可观。

4. 场景选择:面试官想听什么?

在阐述完以上区别后,给出清晰的场景选择建议,是展示你实践经验的加分项。

  • 优先选择 ArrayList
    • 绝大多数场景下,ArrayList 都是首选。它的随机访问性能极好,CPU缓存亲和性也更好(因为内存连续)。
    • 只要不是频繁地在列表开头或中间进行大量插入/删除操作,ArrayList 的性能表现都优于 LinkedList
    • “读多写少”的场景,或者只在列表末尾进行添加/删除,请果断使用 ArrayList
  • 什么时候考虑 LinkedList
    • 当需要大量地在列表的头部和尾部进行添加和删除操作时。这使得 LinkedList 非常适合实现 栈 (Stack)队列 (Queue)双端队列 (Deque)
    • 当有大量使用 Iterator 在遍历过程中进行添加和删除的操作时。

面试官追问:LinkedListadd(index, element) 是 O(n),那它在中间插入的优势何在?

这是一个很好的问题。LinkedList 在“中间插入”的优势仅体现在你已经拥有一个指向那个位置的迭代器 (ListIterator) 的前提下。通过迭代器进行的 add()remove() 操作是 O(1) 的。如果只是通过索引去插入,那么 LinkedList 并没有优势,甚至因为遍历的开销而劣于 ArrayListSystem.arraycopy


总结

特性 ArrayList LinkedList
数据结构 动态数组 双向链表
随机访问 快 (O(1)) 慢 (O(n))
插入/删除 慢 (O(n)) 快 (O(1)) (仅限头尾或已有迭代器)
内存开销 较小,但可能浪费容量 较大 (Node对象开销)
适用场景 绝大多数场景,特别是读多写少 队列/栈实现,大量头尾操作

在面试中,清晰地讲出以上要点,特别是性能对比和场景选择,就能充分展示你对这两种数据结构的深刻理解。