ArrayList 和 LinkedList 有什么区别?一文讲清底层原理

在日常开发中,List 是最常用的数据结构之一。而 ArrayList 与 LinkedList 虽然都实现了 List 接口,但背后的实现机制与使用场景却有本质区别。本文从底层结构、增删查改效率、源码原理到实战建议,全方位拆解两者的差异。


一、底层结构:一个是数组,一个是链表

特性

ArrayList

LinkedList

底层结构

动态数组 Object[]

双向链表

内存布局

连续内存,访问效率高

非连续内存,插入删除效率高

数据访问方式

通过索引随机访问

通过节点指针顺序访问


二、增删查改的性能对比(时间复杂度)

操作

ArrayList

LinkedList

增加尾部

O(1)(均摊)

O(1)

增加中间

O(n)(元素需后移)

O(n)(遍历定位)

删除中间

O(n)(元素需前移)

O(n)(遍历定位)

查找(get)

O(1)

O(n)

🧠 关键总结

  • ArrayList:读写频繁、随机访问性能更佳;

  • LinkedList:适用于频繁插入/删除(尤其头尾);


三、源码实现深度解析

我们从以下关键维度对 ArrayList 与 LinkedList 的源码展开剖析:

3.1 ArrayList 源码剖析

初始化逻辑

// 无参构造
public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
  • 初始并不立即分配数组,真正添加第一个元素时才触发 grow() 扩容逻辑;

  • 默认初始容量是 10(在第一次添加时设置);

  • elementData 是一个 Object[],数组存储的是对象引用。

添加元素逻辑

public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // 确保容量足够
    elementData[size++] = e;
    return true;
}

ensureCapacityInternal 源码:

private void ensureCapacityInternal(int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
    modCount++;
    if (minCapacity - elementData.length > 0)
        grow(minCapacity); // 扩容
}

grow 扩容算法:

private void grow(int minCapacity) {
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + (oldCapacity >> 1); // 扩容为1.5倍
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    elementData = Arrays.copyOf(elementData, newCapacity);
}

🧠 关键点:

  • 增长比例为原始容量的 1.5 倍

  • System.arraycopy() 是主要的性能开销来源;

  • 每次扩容都会创建新数组并复制数据,GC 压力明显。

删除元素逻辑

public E remove(int index) {
    Objects.checkIndex(index, size);
    E oldValue = elementData(index);
    int numMoved = size - index - 1;
    if (numMoved > 0) {
        System.arraycopy(elementData, index+1, elementData, index, numMoved);
    }
    elementData[--size] = null; // help GC
    return oldValue;
}

📌 删除中间元素时:

  • 数组整体后移;

  • null 出被移除的元素,避免内存泄漏;

  • 大数据量删除操作是性能瓶颈。


3.2 LinkedList 源码剖析

核心结构定义

private static class Node<E> {
    E item;
    Node<E> next;
    Node<E> prev;
}
transient Node<E> first;
transient Node<E> last;
  • 双向链表结构,支持从头尾快速插入;

  • 遍历必须从 first 或 last 逐步定位。

添加元素(尾部)逻辑

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++;
}

📌 插入尾部仅需 3 步:

  1. 创建新节点;

  2. 更新前一个 last 的 next;

  3. 重置 last 指针。

查找元素逻辑

public E get(int index) {
    checkElementIndex(index);
    return node(index).item;
}
Node<E> node(int index) {
    // 从靠近 index 的一端开始查找
    if (index < (size >> 1)) {
        Node<E> x = first;
        for (int i = 0; i < index; i++)
            x = x.next;
        return x;
    } else {
        Node<E> x = last;
        for (int i = size - 1; i > index; i--)
            x = x.prev;
        return x;
    }
}

性能影响

  • 任意位置访问需要线性查找;

  • 会优先选择离 index 最近的方向遍历;

  • 对大规模数据结构的 get(i) 不推荐。

删除元素逻辑

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; // help GC
    size--;
    modCount++;
    return element;
}

删除优势

  • 不需要整体移动结构,只改指针;

  • 整体复杂度为 O(1),前提是节点已知。


3.3 modCount 与 fail-fast 机制

两者都维护 modCount 字段,用于结构修改次数计数。

配合 Iterator 实现 fail-fast 机制:

public class Itr implements Iterator<E> {
    int expectedModCount = modCount;
    public E next() {
        checkForComodification(); // 结构变化会抛出异常
    }
}
  • 并发修改会触发 ConcurrentModificationException;

  • 不适合在多线程环境下直接使用,需加锁或使用并发集合。


3.4 小结对比

维度

ArrayList

LinkedList

结构

动态数组

双向链表

插入效率

尾部快,中间慢(需数组移动)

中间快(只改指针),但需遍历定位

访问效率

随机访问快,O(1)

随机访问慢,O(n)

内存

连续空间、空间紧凑

非连续空间,内存碎片大

扩容

自动扩容 + 数组复制(高 GC 压力)

插入不扩容,但频繁 new 对象


四、内存与 GC 行为差异

  • ArrayList 内存连续,局部性好,CPU 缓存命中率高;

  • LinkedList 每个节点单独分配内存,占用更多堆空间;

  • GC 行为上,链表节点回收复杂度高于数组,可能频繁触发 Minor GC。


五、线程安全性说明

默认情况下,两者 都不是线程安全

如需线程安全:

  • 可使用 Collections.synchronizedList(list) 封装;

  • 或者使用 CopyOnWriteArrayList(适合读多写少);


六、实战场景选型建议

场景

推荐使用

频繁随机访问

ArrayList

数据量大 + 尾部频繁添加

ArrayList

插入/删除频繁(尤其头尾)

LinkedList

内存敏感、对局部性要求高

ArrayList

需要遍历/排序操作较多

ArrayList 更高效


七、误用警告 ⚠️

LinkedList 不适合做栈:虽然有 addFirst()/removeFirst(),但在并发或复杂逻辑下容易出错;

不要使用 LinkedList 遍历查找元素:效率远低于数组,尤其是大数据量时;

不要在 ArrayList 删除时用普通 for 循环,建议使用倒序删除或 Iterator 进行安全操作。


八、结合 Java 8+ Stream API 使用注意事项

  • ArrayList 遍历性能优越,配合 parallelStream() 更适合并行流;

  • LinkedList 遍历开销大,不推荐用于流操作(尤其是 map/filter 处理时);


九、结语

虽然 ArrayList 和 LinkedList 都是 List 接口的实现,但差异远比想象中大。了解其底层结构与行为差异,才能写出高性能、低风险的 Java 代码。不要“用着都能跑”就觉得两者无所谓,关键时刻,选对集合能救命!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小健学 Java

你的鼓励是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值