Java中ArrayList和LinkedList的比较

注:Joshua Bloch 就是 LinkedList 的作者

在Java中,ArrayList和LinkedList都是常用的列表实现类,它们都实现了List接口,但在内部工作原理和性能方面有显著差异。

  • ArrayList:基于动态数组实现。随着元素的增加,数组的大小会动态调整。如果数组容量不足时,会分配一个新的更大的数组并复制元素。
  • LinkedList:基于双向链表实现。每个元素是一个节点,包含指向前后元素的引用。

访问速度

  • ArrayList:支持通过索引进行随机访问(get(int index)),由于是基于数组,所以访问速度很快,时间复杂度为O(1)。
  • LinkedList:随机访问时,需要从头部或尾部遍历链表,时间复杂度为O(n),因此访问速度较慢。

内存消耗

  • ArrayList:由于是数组实现,元素仅存储数据,因此内存利用率较高,但在动态扩容时可能会浪费一些内存。
  • LinkedList:每个节点不仅存储数据,还存储两个引用(前一个和后一个节点),因此需要更多的内存。

关于 ArrayList 和 LinkedList 在添加 (add) 和删除 (remove) 操作上的性能比较,传统观点认为由于 ArrayList 需要移动数据,而 LinkedList 只需调整链表指针,因此 LinkedList 更快。然而,实际性能并不是这么简单,两者在不同情况下的表现差异较大,

ArrayList中的随机访问、添加和删除部分源码如下:

java 复制代码
// 获取 index 位置的元素值
public E get(int index) {
    rangeCheck(index); // 首先判断 index 的范围是否合法

    return elementData(index); // 返回 elementData 数组中 index 位置的元素
}

// 将 index 位置的值设为 element,并返回原来的值
public E set(int index, E element) {
    rangeCheck(index); // 检查索引的有效性

    E oldValue = elementData(index); // 保存旧值
    elementData[index] = element; // 将新值 element 赋给 elementData 数组的 index 位置
    return oldValue; // 返回旧值
}

// 将 element 添加到 ArrayList 的指定位置
public void add(int index, E element) {
    rangeCheckForAdd(index); // 检查索引是否适合添加操作

    ensureCapacityInternal(size + 1);  // 确保内部容量足够存储新增的元素,并增加 modCount
    // 将 index 及其后的数据复制到 index+1 的位置,即从 index 开始向后挪了一位
    System.arraycopy(elementData, index, elementData, index + 1, size - index); 
    elementData[index] = element; // 在 index 处插入 element
    size++; // 增加列表大小
}

// 删除 ArrayList 指定位置的元素
public E remove(int index) {
    rangeCheck(index); // 检查索引的有效性

    modCount++; // 增加 modCount
    E oldValue = elementData(index); // 记录被移除元素的旧值

    int numMoved = size - index - 1;
    if (numMoved > 0) {
        // 向左挪一位,index 位置原来的数据已经被覆盖
        System.arraycopy(elementData, index + 1, elementData, index, numMoved);
    }
    // 清空最后一个元素
    elementData[--size] = null; 

    return oldValue; // 返回被移除的旧值
}

LinkedList中的随机访问、添加和删除部分源码如下:

java 复制代码
// 获得第 index 个节点的值
public E get(int index) {
    checkElementIndex(index); // 检查索引的有效性
    return node(index).item; // 返回第 index 个节点的值
}

// 设置第 index 个元素的值
public E set(int index, E element) {
    checkElementIndex(index); // 检查索引的有效性
    Node<E> x = node(index); // 定位到第 index 个节点
    E oldVal = x.item; // 保存旧值
    x.item = element; // 设置新值
    return oldVal; // 返回旧值
}

// 在 index 个节点之前添加新的节点
public void add(int index, E element) {
    checkPositionIndex(index); // 检查索引的有效性
    
    if (index == size) {
        linkLast(element); // 如果在末尾添加,直接调用 linkLast 方法
    } else {
        linkBefore(element, node(index)); // 否则,在指定位置之前添加新节点
    }
}

// 删除第 index 个节点
public E remove(int index) {
    checkElementIndex(index); // 检查索引的有效性
    return unlink(node(index)); // 删除指定节点并返回旧值
}

// 定位 index 处的节点
Node<E> node(int index) {
    // assert isElementIndex(index); // 断言索引的有效性
    // 当 index < size / 2 时,从头开始查找
    if (index < (size >> 1)) {
        Node<E> x = first;
        for (int i = 0; i < index; i++) {
            x = x.next;
        }
        return x;
    } else { // 当 index >= size / 2 时,从尾部开始查找
        Node<E> x = last;
        for (int i = size - 1; i > index; i--) {
            x = x.prev;
        }
        return x;
    }
}
  • 随机访问(get 和 set):ArrayList 远远优于 LinkedList。ArrayList 能在 O(1) 的时间复杂度内完成,而 LinkedList 的查找时间复杂度是 O(n),性能远不如 ArrayList。
  • 插入和删除:这两种操作的性能没有简单的结论。对于 ArrayList,如果插入或删除发生在数组的末尾,操作效率非常高(O(1));但是如果操作发生在数组的中间或开头,则需要移动大量元素(O(n))。LinkedList 的插入和删除操作在理论上更快(O(1)),但由于需要遍历链表查找目标位置,因此查找阶段(O(n))会拖慢整体性能。对于插入或删除操作,LinkedList 在极端情况下(如频繁在中间插入或删除)可能优于 ArrayList,但通常两者的效率没有明显的优劣。

下面通过程序来测试一下两者插入的速度:

java 复制代码
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;

/**
 * 列表插入测试类
 * 本类用于测试在不同类型的列表(ArrayList和LinkedList)中插入元素的性能
 */
public class ListInsertionTest {

    /**
     * 插入测试方法,在指定索引位置插入元素
     * 
     * @param list 待测试的列表
     * @param insertions 插入的元素数量
     * @param index 插入的索引位置
     * @param listType 列表类型,用于输出信息
     */
    private static void testInsertion(List<Integer> list, int insertions, int index, String listType) {
        long startTime = System.nanoTime();

        // 在指定索引位置插入元素
        for (int i = 0; i < insertions; i++) {
            list.add(index, i);
        }

        long endTime = System.nanoTime();
        long duration = (endTime - startTime) / 1_000_000; // 转换为毫秒
        System.out.println(listType + " 在 index=" + index + " 插入 " + insertions + " 个元素,耗时: " + duration + " 毫秒");
    }

    public static void main(String[] args) {
        int initialSize = 10_000; // 初始容量
        int insertions = 10_000;  // 插入的元素数量

        // 初始化 ArrayList 和 LinkedList
        List<Integer> arrayList = new ArrayList<>(initialSize);
        List<Integer> linkedList = new LinkedList<>();

        // 初始化数据,使得列表的初始容量为 initialSize
        for (int i = 0; i < initialSize; i++) {
            arrayList.add(i);
            linkedList.add(i);
        }

        // 测试 ArrayList
        System.out.println("ArrayList:");
        testInsertion(arrayList, insertions, 1000, "ArrayList");  // 在 index=1000 处插入
        testInsertion(arrayList, insertions, 5000, "ArrayList");  // 在 index=5000 处插入
        testInsertion(arrayList, insertions, 9000, "ArrayList");  // 在 index=9000 处插入

        // 测试 LinkedList
        System.out.println("\nLinkedList:");
        testInsertion(linkedList, insertions, 1000, "LinkedList");  // 在 index=1000 处插入
        testInsertion(linkedList, insertions, 5000, "LinkedList");  // 在 index=5000 处插入
        testInsertion(linkedList, insertions, 9000, "LinkedList");  // 在 index=9000 处插入
    }
}
java 复制代码
ArrayList:
ArrayList 在 index=1000 插入 10000 个元素,耗时: 17 毫秒
ArrayList 在 index=5000 插入 10000 个元素,耗时: 21 毫秒
ArrayList 在 index=9000 插入 10000 个元素,耗时: 29 毫秒

LinkedList:
LinkedList 在 index=1000 插入 10000 个元素,耗时: 21 毫秒
LinkedList 在 index=5000 插入 10000 个元素,耗时: 77 毫秒
LinkedList 在 index=9000 插入 10000 个元素,耗时: 148 毫秒

从测试结果来看,ArrayList 在不同插入位置的性能表现明显优于 LinkedList。

  • ArrayList 在进行插入操作时,即使是插入到中间和靠后的位置,仍然表现出较为稳定和较快的速度。
  • LinkedList 的插入操作在前半部分表现较好,但随着插入位置的增加,性能迅速下降,不适合在长列表中频繁插入大量数据。

根据测试结果,ArrayList 更适合在大多数情况下的插入操作。

相关推荐
Scott9999HH9 小时前
【IIoT流量实战】蒸汽管道阀门全关却仍有流量?用 Python 实现涡街信号 FFT 频谱分析与温压全补偿积算网关,深度拆解靠谱的涡街流量计厂家硬核技术标准
开发语言·python
腻害兔9 小时前
【若依项目-产品经理视角】深度拆解 RuoYi-Vue-Pro 商城模块:从商品管理到交易引擎,50 张表撑起一整套电商系统
java·大数据·vue.js·产品经理·ai编程
码智社10 小时前
AES加密原理详解及Java实现加解密实战
java·开发语言
AI云海10 小时前
python 列表、元组、集合和字典
开发语言·python
萧瑟余晖11 小时前
JDK 26 新特性详解
java·开发语言
马优晨12 小时前
Freemarker 完整讲解(后端 Java 模板引擎)
java·开发语言·freemarker·freemarker 完整讲解·freemarker模板引擎
人邮异步社区14 小时前
怎么把C语言学到精通?
c语言·开发语言
心平气和量大福大14 小时前
C#-WPF-控件-TextBox 数据绑定
开发语言·c#·wpf
ttwuai14 小时前
Cursor 生成 CRUD 后,Go 后台接口别只测 200:JWT、RBAC 和 tenant_id 怎么验
开发语言·后端·golang
维天说15 小时前
CLI-Switch 2026年3月版历史设计:Hook、TTY 隔离与 JSON 状态
java·服务器·json