Java集合框架深度解析:LinkedList vs ArrayList 的对决

前言

在 Java 集合框架中,ArrayList​ 和 LinkedList​ 是两种最常用的 List​ 实现类,它们都用于存储和操作有序集合。尽管它们都实现了 List​ 接口,但在底层实现、性能特点和适用场景上存在显著差异。理解这些差异对于编写高效、健壮的代码至关重要。本文将从底层结构、性能、内存占用和适用场景等方面对 ArrayList​ 和 LinkedList​ 进行详细对比,帮助开发者在实际开发中做出更合理的选择。

一、核心区别概览

特性 ArrayList LinkedList
底层数据结构 动态数组 双向链表
内存占用 更紧凑(仅存储数据) 更高(每个元素带两个指针)
随机访问性能 O(1) - 极快 O(n) - 较慢
头部插入/删除 O(n) - 需要移动元素 O(1) - 直接修改指针
尾部插入/删除 O(1) - 快速(摊销时间) O(1) - 快速
中间插入/删除 O(n) - 需要移动元素 O(n) - 需要遍历到位置
迭代器性能 快速 快速
内存局部性 优秀(连续内存) 差(元素分散在内存中)

二、底层数据结构详解

1. ArrayList

arduino 复制代码
// 内部实现
transient Object[] elementData; // 存储元素的数组
private int size;              // 当前元素数量
  • 基于动态可扩展数组
  • 初始容量通常为10
  • 当容量不足时自动扩容(通常是原容量的1.5倍)

源码:类属性

java 复制代码
public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
    //序列号版本号
    @java.io.Serial
    private static final long serialVersionUID = 8683452581122892189L;
​
    //默认初始容量
    private static final int DEFAULT_CAPACITY = 10;
​
    //空数组,指定默认初始化容量为0时赋值给elementData,避免了重复创建空数组
    private static final Object[] EMPTY_ELEMENTDATA = {};
​
    //当调用无参构造方法时,赋值给elementData,主要用于在添加第一个元素前,标记该ArrayList是由无参构造器创建,便于将容量初始化为DEFAULT_CAPACITY,避免了重复创建空数组
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
​
    //实际存放元素的数组
    transient Object[] elementData; // non-private to simplify nested class access
​
    //ArrayList的元素个数
    private int size;
}
​

ArrayList​是List接口的实现类,底层基于数组实现,容量可根据需要动态增加,相当于动态数组。ArrayList​继承于AbstractList​,并且还实现了Cloneable​、Serializable​、RandomAccess​接口。

  • List:表明是列表数据结构,可以通过下标对元素进行添加删除或查找。
  • Serializable:表示可以进行序列化和反序列化操作,可以把对象与字节流相互转换。
  • RandomAccess:有这个接口标记的List表示可以支持快速随机访问,即通过元素下标可以直接得到元素内容。
  • Cloneable:表示支持拷贝,可以通过浅拷贝或深拷贝来复制对象的副本。

2. LinkedList

java 复制代码
// 内部节点结构
private static class Node<E> {
    E item;         // 存储的数据
    Node<E> next;   // 指向下一个节点
    Node<E> prev;   // 指向上一个节点
}
​
// 链表属性
transient Node<E> first; // 头节点
transient Node<E> last;  // 尾节点
transient int size = 0;  // 元素数量
  • 基于双向链表实现
  • 每个元素都包装在节点对象中
  • 节点包含前后指针(增加内存开销)

三、性能对比详解

1. 访问元素(get 操作)

scss 复制代码
// ArrayList 直接通过索引访问数组
public E get(int index) {
    Objects.checkIndex(index, size);
    return elementData(index); // 直接数组访问
}
​
// LinkedList 需要遍历链表
public E get(int index) {
    checkElementIndex(index);
    return node(index).item; // 需要遍历到指定位置
}
  • ArrayList:O(1) 时间复杂度,直接通过数组索引访问
  • LinkedList:O(n) 时间复杂度,平均需要遍历半个列表

2. 插入/删除操作

尾部操作
typescript 复制代码
// ArrayList 尾部添加
public boolean add(E e) {
    modCount++;
    add(e, elementData, size); // 通常为O(1),扩容时O(n)
    return true;
}
​
// LinkedList 尾部添加
public boolean add(E e) {
    linkLast(e); // 总是O(1)
    return true;
}
  • 两者在尾部操作都很高效(O(1))
  • ArrayList 在需要扩容时有临时性能下降
头部操作
ini 复制代码
// ArrayList 头部插入
public void add(int index, E element) {
    rangeCheckForAdd(index);
    modCount++;
    final int s;
    Object[] elementData;
    // 需要移动所有后续元素
    System.arraycopy(elementData, index,
                     elementData, index + 1,
                     s - index);
}
​
// LinkedList 头部插入
private void linkFirst(E e) {
    final Node<E> f = first;
    final Node<E> newNode = new Node<>(null, e, f);
    first = newNode;
    if (f == null)
        last = newNode;
    else
        f.prev = newNode;
}
  • ArrayList:O(n) - 需要移动所有后续元素
  • LinkedList:O(1) - 只需修改几个指针

3. 内存占用

  • ArrayList

    • 每个元素占用约 4 字节(引用大小)
    • 少量额外空间(size 计数等)
    • 可能有未使用的预留空间
  • LinkedList

    • 每个元素需要额外 24 字节(12字节对象头 + 前后指针各4字节 + 4字节数据引用)
    • 内存分散,缓存不友好

4. 迭代性能对比

ini 复制代码
// 遍历 100,000 个元素的时间对比
List<Integer> arrayList = new ArrayList<>();
List<Integer> linkedList = new LinkedList<>();
​
// 填充数据...
​
long start = System.nanoTime();
for (int i : arrayList) { /* 遍历操作 */ }
long arrayTime = System.nanoTime() - start;
​
start = System.nanoTime();
for (int i : linkedList) { /* 遍历操作 */ }
long linkedTime = System.nanoTime() - start;
​
System.out.println("ArrayList遍历时间: " + arrayTime + "ns");
System.out.println("LinkedList遍历时间: " + linkedTime + "ns");
  • 两者迭代性能接近(O(n))
  • ArrayList 通常更快(更好的内存局部性)
  • LinkedList 在迭代时删除元素更高效

使用场景推荐
使用 ArrayList 当:

✅ 需要频繁随机访问元素(按索引) ✅ 主要在列表尾部添加/删除元素 ✅ 内存资源紧张 ✅ 需要最小化迭代开销 ✅ 元素数量相对稳定

使用 LinkedList 当:

✅ 需要频繁在头部插入/删除元素 ✅ 需要实现队列或双端队列功能 ✅ 列表中间需要频繁插入/删除 ✅ 内存资源充足 ✅ 主要使用迭代器进行遍历和修改

最佳实践
  1. 初始化容量(ArrayList):

    arduino 复制代码
    // 预估大小以减少扩容次数
    List<String> list = new ArrayList<>(1000);
  2. 使用迭代器删除(LinkedList):

    ini 复制代码
    Iterator<Integer> iter = linkedList.iterator();
    while (iter.hasNext()) {
        if (iter.next() % 2 == 0) {
            iter.remove(); // O(1) 操作
        }
    }
  3. 避免使用索引循环(LinkedList):

    scss 复制代码
    // 差 - O(n^2) 性能
    for (int i = 0; i < linkedList.size(); i++) {
        linkedList.get(i); // 每次都是O(n)
    }
    ​
    // 好 - O(n) 性能
    for (Integer num : linkedList) {
        // 使用增强for循环
    }
  4. 考虑替代方案

    arduino 复制代码
    // 需要频繁头部操作时
    Deque<String> deque = new ArrayDeque<>(); // 比LinkedList更高效
    
    // 需要线程安全时
    List<String> safeList = Collections.synchronizedList(new ArrayList<>());

总结

维度 ArrayList 优势 LinkedList 优势
随机访问 ⭐⭐⭐⭐⭐ 极快 ⭐ 很慢
头部操作 ⭐ 很慢 ⭐⭐⭐⭐⭐ 极快
内存效率 ⭐⭐⭐⭐ 较高效 ⭐ 较低效
迭代性能 ⭐⭐⭐⭐ 很快 ⭐⭐⭐ 较快
中间修改 ⭐⭐ 较慢 ⭐⭐⭐ 较快(使用迭代器)

简单决策指南

  • 90% 的情况选择 ArrayList(Java 标准库和大多数框架的默认选择)
  • 需要实现队列/栈功能时选择 LinkedListArrayDeque
  • 在极端性能敏感场景进行基准测试

总的来说,ArrayList​ 和 LinkedList​ 各有优缺点,适用的场景也有所不同。选择哪个集合类应根据具体需求来决定。如果你需要频繁进行随机访问,ArrayList​ 是更好的选择;而如果你需要频繁进行插入和删除操作,尤其是在列表的头部或尾部,LinkedList​ 会更合适。

在实际编程中,理解它们的区别并根据需求合理选择数据结构,可以大大提高程序的性能和效率。希望本文能够帮助大家更好地理解这两种常见的 Java 集合类,为编写高效、灵活的代码提供参考。

相关推荐
张先shen1 小时前
Spring Boot集成Redis:从配置到实战的完整指南
spring boot·redis·后端
Dolphin_海豚1 小时前
一文理清 node.js 模块查找策略
javascript·后端·前端工程化
cainiao0806051 小时前
Java 大视界:基于 Java 的大数据可视化在智慧城市能源消耗动态监测与优化决策中的应用(2025 实战全景)
java
长风破浪会有时呀2 小时前
记一次接口优化历程 CountDownLatch
java
EyeDropLyq2 小时前
线上事故处理记录
后端·架构
云朵大王2 小时前
SQL 视图与事务知识点详解及练习题
java·大数据·数据库
我爱Jack2 小时前
深入解析 LinkedList
java·开发语言
27669582924 小时前
tiktok 弹幕 逆向分析
java·python·tiktok·tiktok弹幕·tiktok弹幕逆向分析·a-bogus·x-gnarly
用户40315986396634 小时前
多窗口事件分发系统
java·算法
用户40315986396634 小时前
ARP 缓存与报文转发模拟
java·算法