ArrayList 与 LinkedList 源码深度对比解析

ArrayList 与 LinkedList 源码深度对比解析

前言

本文基于 JDK 1.8 源码,从底层数据结构、核心方法实现、扩容机制、增删改查效率、使用场景 五个维度,对 ArrayListLinkedList 进行全方位对比,方便后续快速回顾和使用。

一、核心定义与继承结构

1. 继承体系

java 复制代码
// ArrayList 继承结构
public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable

// LinkedList 继承结构
public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable
  • 关键区别
    1. ArrayList 实现了 RandomAccess 接口(标记接口,代表支持随机访问);
    2. LinkedList 实现了 Deque 接口(代表支持双端队列、栈功能)。

2. 核心定位

  • ArrayList动态数组,基于数组实现,支持快速随机访问;
  • LinkedList双向链表,基于链表节点实现,增删效率高,无固定容量限制。

二、底层数据结构源码对比

1. ArrayList 底层:Object 数组

java 复制代码
public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
    // 默认初始化容量
    private static final int DEFAULT_CAPACITY = 10;
    // 空数组(用于无参构造)
    private static final Object[] EMPTY_ELEMENTDATA = {};
    // 默认空数组
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
    // 【核心】存储数据的底层数组
    transient Object[] elementData;
    // 集合实际元素个数
    private int size;
}
  • 本质:连续内存空间的数组,所有操作围绕这个数组展开。

2. LinkedList 底层:双向链表节点

java 复制代码
public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable {
    // 链表长度
    transient int size = 0;
    // 【核心】链表头节点
    transient Node<E> first;
    // 【核心】链表尾节点
    transient Node<E> last;

    // 双向链表节点内部类
    private static class Node<E> {
        E item;         // 存储的数据
        Node<E> next;   // 后继节点
        Node<E> prev;   // 前驱节点

        Node(Node<E> prev, E element, Node<E> next) {
            this.item = element;
            this.next = next;
            this.prev = prev;
        }
    }
}
  • 本质:非连续内存空间,每个节点包含「数据、前驱、后继」三个属性。

三、初始化方式源码对比

1. ArrayList 初始化

(1)无参构造(最常用)
java 复制代码
public ArrayList() {
    // 初始化为空数组,第一次添加元素时才扩容
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
  • 懒加载:初始容量为 0,首次添加元素时扩容为默认容量 10
(2)指定容量构造
java 复制代码
public ArrayList(int initialCapacity) {
    if (initialCapacity > 0) {
        // 直接创建指定长度的数组
        this.elementData = new Object[initialCapacity];
    } else if (initialCapacity == 0) {
        this.elementData = EMPTY_ELEMENTDATA;
    } else {
        throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity);
    }
}

2. LinkedList 初始化

java 复制代码
// 无参构造(唯一常用构造)
public LinkedList() {}
  • 无容量概念:初始化时无节点,添加元素时才创建节点。

四、核心方法源码深度解析

1. 添加元素(add)

(1)ArrayList add 源码
java 复制代码
public boolean add(E e) {
    // 1. 检查容量,不足则扩容
    ensureCapacityInternal(size + 1);
    // 2. 直接赋值到数组末尾
    elementData[size++] = e;
    return true;
}

// 扩容核心方法
private void ensureCapacityInternal(int minCapacity) {
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

private void ensureExplicitCapacity(int minCapacity) {
    modCount++;
    // 所需容量 > 当前数组长度,触发扩容
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

// 扩容逻辑:新容量 = 原容量的 1.5 倍
private void grow(int minCapacity) {
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + (oldCapacity >> 1); // 等价于 oldCapacity * 1.5
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    // 数组拷贝,生成新数组
    elementData = Arrays.copyOf(elementData, newCapacity);
}
  • 添加流程:检查容量 → 扩容(如需) → 数组末尾赋值;
  • 缺点 :扩容时需要数组拷贝,消耗性能;中间插入需要移动元素。
(2)LinkedList add 源码
java 复制代码
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++;
}
  • 添加流程:创建新节点 → 修改前驱/后继指针;
  • 优点 :无需扩容,无数组拷贝,尾部添加效率极高
  • 缺点:中间插入需要先遍历找到目标位置。

2. 根据索引获取元素(get)

(1)ArrayList get 源码
java 复制代码
public E get(int index) {
    // 检查索引是否越界
    rangeCheck(index);
    // 直接通过数组下标访问,O(1)
    return elementData(index);
}

E elementData(int index) {
    return (E) elementData[index];
}
  • 时间复杂度O(1),支持随机访问,效率极高。
(2)LinkedList get 源码
java 复制代码
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;
    }
}
  • 时间复杂度O(n),即使二分优化,仍需遍历,随机访问效率极低

3. 根据索引删除元素(remove)

(1)ArrayList remove 源码
java 复制代码
public E remove(int index) {
    rangeCheck(index);
    modCount++;
    E oldValue = elementData(index);
    // 计算需要移动的元素个数
    int numMoved = size - index - 1;
    if (numMoved > 0)
        // 数组拷贝,移动后续元素,填补空位
        System.arraycopy(elementData, index+1, elementData, index, numMoved);
    // 置空最后一个元素,帮助GC
    elementData[--size] = null;
    return oldValue;
}
  • 缺点 :删除非末尾元素,需要移动大量元素,效率低。
(2)LinkedList remove 源码
java 复制代码
public E remove(int index) {
    checkElementIndex(index);
    // 1. 找到目标节点
    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;
        x.prev = null;
    }

    if (next == null) {
        last = prev;
    } else {
        next.prev = prev;
        x.next = null;
    }

    x.item = null; // 置空,帮助GC
    size--;
    modCount++;
    return element;
}
  • 优点 :仅需修改指针引用,无需移动元素,删除效率高。

五、扩容机制对比

集合 扩容触发条件 扩容规则 性能消耗
ArrayList 数组容量不足 新容量 = 原容量 × 1.5 高(需数组拷贝,开辟新连续内存)
LinkedList 无扩容机制 按需创建节点,无容量限制
  • 关键结论ArrayList 有固定容量,必须扩容;LinkedList 无容量上限,天然支持动态添加。

六、核心特性总结表

特性 ArrayList LinkedList
底层结构 动态Object数组 双向链表
随机访问 支持(O(1)) 不支持(O(n))
尾部增删 高效(扩容时略低) 极高(O(1))
中间增删 低效(需移动元素) 高效(仅改指针)
内存占用 连续内存,尾部有空闲空间 非连续内存,每个节点多存2个引用
线程安全 非安全 非安全
实现接口 RandomAccess Deque(双端队列)

七、使用场景选择

1. 优先使用 ArrayList

  • 大量查询、根据索引访问操作;
  • 数据频繁尾部添加/删除
  • 内存有限,需要连续存储(节省节点指针的内存开销)。

2. 优先使用 LinkedList

  • 大量中间插入/删除操作;
  • 需要实现栈、队列、双端队列LinkedList 实现了 Deque 接口);
  • 数据量动态极大,无法预估容量(避免 ArrayList 频繁扩容)。

八、关键注意事项

  1. ArrayList 避坑
    • 无参构造初始容量为 0,首次添加才扩容为 10;
    • 预估数据量时,指定初始容量 ,减少扩容次数(如 new ArrayList<>(100))。
  2. LinkedList 避坑
    • 不要用 for 循环遍历(每次 get(i) 都遍历链表),推荐用迭代器/增强for
    • 内存开销比 ArrayList 大(每个节点存储前驱、后继)。
  3. 两者均非线程安全 ,多线程环境下使用 CopyOnWriteArrayList 或加锁。

总结

  1. 底层核心ArrayList 是数组(连续内存、随机访问快),LinkedList 是双向链表(非连续内存、增删快);
  2. 效率核心 :查多用 ArrayList,增删多用 LinkedList
  3. 源码关键ArrayList 核心是扩容+数组拷贝LinkedList 核心是节点指针修改
相关推荐
人间打气筒(Ada)1 天前
如何基于 Go-kit 开发 Web 应用:从接口层到业务层再到数据层
开发语言·后端·golang
2501_924952691 天前
代码生成器优化策略
开发语言·c++·算法
清风徐来QCQ1 天前
八股文(1)
java·开发语言
zdl6861 天前
springboot集成onlyoffice(部署+开发)
java·spring boot·后端
lsx2024061 天前
网站主机技术
开发语言
摇滚侠1 天前
你是一名 java 程序员,总结定义数组的方式
java·开发语言·python
xyq20241 天前
Vue3 条件语句详解
开发语言
架构师沉默1 天前
AI 让程序员更轻松了吗?
java·后端·架构
浩浩kids1 天前
R•Homework
开发语言·r语言
qq_416018721 天前
设计模式在C++中的实现
开发语言·c++·算法