ArrayList vs LinkedList:底层原理与实战选择指南

ArrayList vs LinkedList:底层原理与实战选择指南

概述

ArrayList 和 LinkedList 是 Java 集合框架中最常用的两个 List 实现类,看似功能相似(都可存储有序、可重复的元素),但底层结构截然不同,导致性能差异巨大。

本文将从底层实现核心方法原理时间复杂度三个维度拆解两者的区别,帮你彻底搞懂 "为什么 ArrayList 查找快,LinkedList 插入删除快",以及如何根据场景选择。

一、底层结构:数组 vs 双向链表

1. ArrayList:动态数组

底层基于连续的数组实现,所有元素在内存中占用连续空间,通过索引(下标)直接访问。

可以理解为 "一排连续的抽屉",每个抽屉有编号(索引),能直接找到第 n 个抽屉:

复制代码
索引:0   1   2   3   ...  
元素:A → B → C → D → ...(内存地址连续)  

关键特性

  • 数组容量固定,ArrayList 会在容量不足时自动扩容(默认扩容为原容量的 1.5 倍)。

  • 必须预留一定的空闲空间(避免频繁扩容),可能造成内存浪费。

2. LinkedList:双向链表

底层基于双向链表实现,元素(Node 节点)在内存中分散存储,每个节点包含:

  • 数据域(存储元素值)

  • 前驱指针(prev):指向前一个节点

  • 后继指针(next):指向后一个节点

可以理解为 "串起来的珠子",每个珠子记得前一个和后一个珠子的位置:

复制代码
Node1       Node2       Node3  
┌───┐       ┌───┐       ┌───┐  
│ A │◄─────►│ B │◄─────►│ C │  
└───┘       └───┘       └───┘  
(prev=null)  (prev=Node1)  (prev=Node2)  
(next=Node2) (next=Node3)  (next=null)  

关键特性

  • 元素无需连续存储,内存利用率更高(按需分配节点)。

  • 节点间通过指针关联,访问元素需从头部 / 尾部遍历。

二、核心方法原理:为什么性能差异大?

1. 查找元素(get (int index))

ArrayList:直接定位(O (1))

基于数组的随机访问特性,通过索引直接计算内存地址:

java 复制代码
// ArrayList.get() 核心源码
public E get(int index) {
    rangeCheck(index); // 检查索引是否越界
    return elementData(index); // 直接返回数组中index位置的元素
}

// 数组访问:O(1)时间复杂度
E elementData(int index) {
    return (E) elementData[index];
}

举例:要找第 3 个元素,直接访问数组下标 2(索引从 0 开始),一步到位。

LinkedList:遍历查找(O (n))

链表没有索引,必须从头部(或尾部)逐个遍历节点:

java 复制代码
// LinkedList.get() 核心源码
public E get(int index) {
    checkElementIndex(index);
    return node(index).item; // 先找到节点,再返回数据
}

// 查找节点:需遍历
Node<E> node(int 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;
    }
}

举例:要找第 1000 个元素,需从头部开始,逐个移动指针 999 次,效率随元素数量增加而下降。

2. 插入元素(add (int index, E element))

ArrayList:可能需要移动元素(O (n))
  • 如果插入位置在末尾:直接添加(O (1),但需考虑扩容耗时)。

  • 如果插入位置在中间:需移动插入点后的所有元素,腾出位置:

java 复制代码
// ArrayList.add() 核心源码(插入中间)
public void add(int index, E element) {
    rangeCheckForAdd(index);
    ensureCapacityInternal(size + 1); // 确保容量足够
    // 复制数组:将index后的元素向后移动1位(耗时操作)
    System.arraycopy(elementData, index, elementData, index + 1, size - index);
    elementData[index] = element; // 插入新元素
    size++;
}

举例:在容量为 1000 的数组中,向第 500 位插入元素,需移动 500 个元素,效率低。

LinkedList:只需修改指针(O (1))

无论插入位置在哪,只需修改相邻节点的指针,无需移动其他元素:

java 复制代码
// LinkedList.add() 核心源码(插入中间)
public void add(int index, E element) {
    checkPositionIndex(index);
    if (index == size) // 插入末尾
        linkLast(element);
    else // 插入中间
        linkBefore(element, node(index)); // 先找到目标节点,再修改指针
}

// 插入节点到succ之前
void linkBefore(E e, Node<E> succ) {
    final Node<E> pred = succ.prev;
    final Node<E> newNode = new Node<>(pred, e, succ);
    succ.prev = newNode; // 后节点的prev指向新节点
    if (pred == null) // 如果是头节点
        first = newNode;
    else
        pred.next = newNode; // 前节点的next指向新节点
    size++;
}

举例:在第 1000 个节点前插入新节点,只需修改第 999 个和第 1000 个节点的指针,两步完成。

3. 删除元素(remove (int index))

ArrayList:需移动元素(O (n))

删除中间元素后,需将后续元素向前移动,填补空缺:

java 复制代码
// ArrayList.remove() 核心源码
public E remove(int index) {
    rangeCheck(index);
    modCount++;
    E oldValue = elementData(index);
    int numMoved = size - index - 1;
    if (numMoved > 0)
        // 移动元素:将index后的元素向前移动1位
        System.arraycopy(elementData, index + 1, elementData, index, numMoved);
    elementData[--size] = null; // 清空最后一位,帮助GC
    return oldValue;
}
LinkedList:修改指针(O (1))

找到目标节点后,只需断开其与前后节点的关联:

java 复制代码
// LinkedList.remove() 核心源码
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; // 前节点的next指向后节点
        x.prev = null;
    }
    if (next == null) { // 尾节点
        last = prev;
    } else {
        next.prev = prev; // 后节点的prev指向前节点
        x.next = null;
    }
    x.item = null; // 清空数据,帮助GC
    size--;
    return element;
}

三、时间复杂度对比表

操作 ArrayList LinkedList 性能差异原因
查找(get) O (1)(随机访问) O (n)(遍历) ArrayList 直接用索引,LinkedList 需遍历
末尾插入(add) O (1)(无扩容时) O(1) 两者效率相近
中间插入(add) O (n)(移动元素) O (1)(改指针) ArrayList 需移动元素,LinkedList 仅改指针
中间删除(remove) O (n)(移动元素) O (1)(改指针) 同插入逻辑
遍历(迭代器) O(n) O(n) 遍历次数相同,ArrayList 缓存友好略快

四、使用场景选择策略

  1. 优先选 ArrayList 的场景
    • 频繁查询(get 操作多),如展示列表、数据检索。
    • 元素数量固定或变化不大(避免频繁扩容)。
    • 内存充足,可接受一定的空间浪费。
  1. 优先选 LinkedList 的场景
    • 频繁在中间插入 / 删除(如实现队列、栈、链表结构)。
    • 元素数量动态变化大,且内存紧张(无需预留空间)。
  1. 特殊注意
    • 即使选 LinkedList,也应避免通过索引(get (index))频繁访问元素(O (n) 效率低),建议用迭代器遍历。
    • ArrayList 的扩容会消耗额外时间(复制数组),可初始化时指定容量(new ArrayList<>(1000))优化。

总结:核心要点速览

对比维度 ArrayList LinkedList
底层结构 动态数组(连续内存) 双向链表(分散内存)
核心优势 查找快(O (1)) 插入 / 删除快(中间位置 O (1))
内存特性 需预留空间,可能浪费 按需分配,内存利用率高
适用场景 读多写少 写多(中间插入 / 删除)读少

记住:数组适合查,链表适合改,根据操作频率选择,而非盲目跟风。实际开发中,ArrayList 因查询效率高,使用场景更广泛,但在队列、栈等场景中,LinkedList 是更好的选择。

相关推荐
灵魂猎手15 分钟前
9. Mybatis与Spring集成原理解析
java·后端·源码
AAA修煤气灶刘哥17 分钟前
避坑!线程 / 线程池从入门到华为云实战,面试官听了都点头
java·后端·面试
Agome991 小时前
Docker之nginx安装
java·nginx·docker
java1234_小锋2 小时前
说说你对Integer缓存的理解?
java·开发语言
至此流年莫相忘2 小时前
TypeReference 泛型的使用场景及具体使用流程
java·开发语言·spring boot
Warren982 小时前
Spring Boot 拦截器返回中文乱码的解决方案(附全局优化思路)
java·网络·spring boot·redis·后端·junit·lua
练习时长一年3 小时前
SpringMVC相关自动配置
java·spring boot·后端
bemyrunningdog3 小时前
SpringCloud架构实战:从核心到前沿
java
都叫我大帅哥3 小时前
动态规划:从懵逼到装逼,一篇让你彻底搞懂DP的终极指南
java·算法