乐于分享
好东西不私藏

ArrayList 源码解析:动态数组 + 扩容机制,为什么查快插慢?

ArrayList 源码解析:动态数组 + 扩容机制,为什么查快插慢?

哈喽,各位程序员伙伴~ 👋

上一篇我们拆解了 LinkedList 的双向链表结构,搞懂了它「头尾增删快、查询慢」的核心原因,也厘清了它和 ArrayList 的场景适配差异。而作为 Java 集合中使用频率最高的实现类,ArrayList 几乎是后端开发的「必修课」—— 日常开发的列表查询、数据展示,九成以上的场景都会用到它。

但你真的懂 ArrayList 吗?

  • 它的「动态数组」到底怎么实现的?为什么能实现下标随机访问?
  • 扩容机制的底层逻辑是什么?每次扩容会带来哪些性能开销?
  • 为什么同样是 O (n),ArrayList 中间增删比 LinkedList 更慢?
  • 看似万能的 ArrayList,藏着哪些容易踩坑的性能陷阱?

这篇文章,我们延续「痛点开篇 + 通俗类比 + 源码精读 + 实战避坑」的风格,从底层结构核心操作,从扩容机制实战选型,彻底拆解 ArrayList(基于 JDK8 源码),搞懂它「查快插慢」的本质,以及那些被忽略的性能优化点和高频坑点。

一、开篇痛点:为什么 ArrayList 是「默认首选」,却也最容易用错?

ArrayList 能成为集合框架的「顶流」,核心原因是契合绝大多数业务场景—— 我们日常开发中,「查询多、增删少、尾部增删为主」的场景占比超 90%,而这正是 ArrayList 的「主场」。

但看似简单的 ArrayList,却藏着很多容易被忽略的问题:

  • 新手无脑用 add(int index, E element) 做中间插入,导致系统性能瓶颈;
  • 不知道 ArrayList 初始化容量的重要性,频繁扩容触发数组拷贝,浪费内存和 CPU;
  • 误以为「ArrayList 增删都是 O (n),随便用」,却忽略了扩容和元素移动的隐性开销;
  • 用 ArrayList 存储百万级数据,却没发现底层数组的冗余空间导致的内存浪费。

先看一组同场景性能对比数据(基于 JDK8,10 万次操作,数据量 10 万),直观感受 ArrayList 的优势与短板:

操作类型
ArrayList 耗时(ms)
LinkedList 耗时(ms)
性能优势方
核心原因
随机下标查询
1
25
ArrayList
数组下标随机访问,O (1)
尾部插入
5
6
基本持平
均为 O (1) 操作
中间插入(下标 5 万)
421
312
LinkedList
ArrayList 需移动 5 万元素,LinkedList 仅遍历 5 万节点
头部插入
1286
8
LinkedList
ArrayList 需移动全部 10 万元素,O (n) 极致开销
迭代器遍历
3
4
ArrayList
数组连续内存,缓存命中率更高

核心结论:ArrayList 的「快」是有边界的,它的优势集中在随机查询、尾部增删、连续遍历,而中间 / 头部增删是天生短板 —— 这一切的根源,都在于它的底层结构:动态数组

二、核心结构:动态数组的底层实现(源码拆解 + 通俗类比)

ArrayList 的本质是对数组的封装与增强—— 普通数组是「固定长度」的,一旦创建无法扩容,而 ArrayList 实现了「动态扩容」,让数组能根据元素数量自动调整长度,这也是它和普通数组的核心区别。

1. 通俗类比:可弹性扩容的「储物柜」

把 ArrayList 想象成一组带编号的「储物柜」:

  • 每个储物柜有唯一编号(对应数组下标),从 0 开始依次递增,支持直接按编号找柜子(随机访问);
  • 储物柜的总数量就是「数组容量」,初始时会规划一定数量的柜子(初始容量);
  • 当储物柜被放满时,管理员会找一个更大的空间,把所有物品从旧柜子搬到新的更大的储物柜组(扩容 + 数组拷贝),这个过程会有额外开销;
  • 往储物柜中间放 / 拿物品时,需要把后面的物品全部往后 / 往前挪一位(中间增删的元素移动),物品越多,挪动越耗时。

这个类比完美对应 ArrayList 的核心特性:下标随机访问(快)、扩容拷贝(隐性开销)、中间增删元素移动(慢)

2. 源码精读:ArrayList 的核心属性与底层数组

基于 JDK8 源码,ArrayList 的核心属性仅有几个,却决定了它的所有核心行为,我们逐行拆解:

public class ArrayList<E> extends AbstractList<E>        implements List<E>, RandomAccess, Cloneable, java.io.Serializable {    // 序列化ID    private static final long serialVersionUID = 8683452581122892189L;    // 默认初始容量:创建无参构造的ArrayList时,初始底层数组为EMPTY_ELEMENTDATA,首次添加元素时扩容为10    private static final int DEFAULT_CAPACITY = 10;    // 空数组常量:无参构造时的初始数组,区别于DEFAULTCAPACITY_EMPTY_ELEMENTDATA,用于区分首次扩容逻辑    private static final Object[] EMPTY_ELEMENTDATA = {};    // 默认空数组常量:带初始容量为0的构造方法时使用    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};    // 核心:ArrayList的底层存储容器——真正存储元素的数组,transient表示不参与序列化    transient Object[] elementData;     // 实际存储的元素个数,注意:size ≤ 数组容量(elementData.length)    private int size;    // 带初始容量的构造方法:指定初始数组大小    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);        }    }    // 无参构造方法:初始数组为DEFAULTCAPACITY_EMPTY_ELEMENTDATA,首次add时扩容为10    public ArrayList() {        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;    }}

关键核心结论(必须吃透)

  1. 底层核心是 Object 数组
    elementData 是真正存储元素的容器,所有增删查操作最终都是对这个数组的操作;
  2. size ≠ 数组容量
    size 是实际元素个数,elementData.length 是数组总容量,二者的差值就是「数组冗余空间」;
  3. 默认初始容量的坑
    无参构造的 ArrayList,初始数组是空的,并非直接创建容量为 10 的数组,而是首次调用 add 方法时才会扩容为 10(懒加载优化);
  4. 标记接口的意义
    实现 RandomAccess 接口 —— 这是一个标记接口,没有任何方法,仅表示该集合「支持快速随机访问」,遍历工具类(如 Collections)会根据该接口选择最优遍历方式(下标遍历 / 迭代器遍历)。

三、核心操作:为什么查快插慢?(源码 + 时间复杂度分析)

ArrayList 所有核心操作的性能差异,都源于数组的固有特性支持下标随机访问(O (1)),但增删需要移动元素(O (n))。我们拆解最常用的「查、增、删」操作,结合源码讲清原理和性能。

1. 查询操作:O (1) 快到极致,随机访问的天花板

ArrayList 的查询是所有线性集合中效率最高的,核心原因是底层数组的下标随机访问特性—— 通过下标可以直接定位到数组的内存地址,无需任何遍历,时间复杂度稳定 O (1)。

核心查询方法 get(int index) 源码(JDK8 精简版):

public E get(int index) {    rangeCheck(index); // 校验下标合法性:index < size,否则抛IndexOutOfBoundsException    return elementData(index); // 直接通过下标获取数组元素}// 私有方法:直接返回数组指定下标元素,真正的O(1)操作@SuppressWarnings("unchecked")elementData(int index) {    return (E) elementData[index];}// 下标校验:仅校验是否越界,不校验是否为负数(负数会直接触发数组本身的ArrayIndexOutOfBoundsException)private void rangeCheck(int index) {    if (index >= size)        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));}

关键细节

  • 校验仅判断 index >= size,因为数组本身会对负数下标抛异常,无需额外处理,减少性能开销;
  • 无任何遍历、循环操作,直接通过「数组名 [下标]」获取元素,这是最底层的内存访问方式,效率拉满;
  • 实现 RandomAccess 标记接口后,工具类会优先选择下标遍历,而非迭代器遍历,进一步提升效率。

对比 LinkedList:LinkedList 的 get(int index) 需遍历链表(即使优化了头尾遍历),时间复杂度 O (n),在大数据量下,查询效率和 ArrayList 天差地别。

2. 插入操作:尾部 O (1) 真香,中间 / 头部 O (n) 拉胯

ArrayList 的插入操作分尾部插入指定下标插入,二者的性能天差地别,核心差异在于「是否需要移动元素」。

(1)尾部插入:add (E e)(理想情况 O (1),扩容时 O (n))

这是 ArrayList 最常用、性能最好的插入方式,源码逻辑(精简版):

public boolean add(E e) {    ensureCapacityInternal(size + 1); // 确保数组容量足够,不足则扩容    elementData[size++] = e; // 直接将元素放入数组尾部的空闲位置,O(1)    return true;}

核心两步

  1. 容量校验
    调用 ensureCapacityInternal 检查数组是否有空闲空间,若 size+1 > elementData.length,则触发扩容机制
  2. 元素赋值
    直接将元素赋值给 elementData[size],然后 size++,无任何元素移动,纯 O (1) 操作。

隐性开销:如果触发扩容,会执行「新数组创建 + 旧数组元素拷贝」,这个过程是 O (n) 时间复杂度,这也是为什么建议提前指定 ArrayList 初始容量的核心原因。

(2)指定下标插入:add (int index, E element)(固定 O (n))

这是 ArrayList 的性能短板,也是最容易用错的方法,源码逻辑(精简版):

public void add(int index, E element) {    rangeCheckForAdd(index); // 校验下标合法性:0 ≤ index ≤ size    ensureCapacityInternal(size + 1); // 容量校验,不足则扩容    // 核心开销:数组拷贝,移动元素——将index后的所有元素往后挪一位    System.arraycopy(elementData, index, elementData, index + 1, size - index);    elementData[index] = element; // 插入新元素    size++; // 元素个数+1}

性能核心System.arraycopy 是本地方法(native),虽然效率比手动循环高,但本质还是移动元素—— 需要把下标 index 后的 size-index 个元素全部往后移动一位,腾出位置给新元素。

  • 头部插入(index=0):需要移动全部 size 个元素,是最耗时的插入方式,时间复杂度 O (n);
  • 中间插入(index=size/2):需要移动一半元素,时间复杂度仍为 O (n);
  • 尾部插入(index=size):等价于 add(E e),无需移动元素,O (1)。

对比 LinkedList:二者中间插入均为 O (n),但 ArrayList 是「移动元素」,LinkedList 是「遍历找节点」,在大数据量下,ArrayList 中间插入的耗时通常更高(元素移动的开销 > 链表遍历的开销)。

3. 删除操作:和插入同理,中间 / 头部删除必移动元素(O (n))

删除操作的性能逻辑和插入完全对称,核心开销仍是元素移动,源码以 remove(int index) 为例(精简版):

public E remove(int index) {    rangeCheck(index); // 下标校验    modCount++; // 结构修改次数,用于迭代器并发修改校验    E oldValue = elementData(index); // 获取待删除元素    // 计算需要移动的元素个数:index后的元素个数    int numMoved = size - index - 1;    if (numMoved > 0)        // 核心开销:将index后的元素往前挪一位,覆盖待删除元素        System.arraycopy(elementData, index+1, elementData, index, numMoved);    // 置空最后一个元素,让GC回收,避免内存泄漏    elementData[--size] = null;     return oldValue;}

关键细节

  1. 元素移动
    numMoved > 0 表示待删除元素不是最后一个,需要将后续元素往前移动一位,覆盖待删除元素,时间复杂度 O (n);
  2. 内存优化
    删除后会将 elementData[--size] 置为 null,让垃圾回收器能回收该对象,避免「数组持有对象引用导致的内存泄漏」;
  3. 快速失败
    修改 modCount,如果迭代器遍历过程中发生删除操作,会触发 ConcurrentModificationException,避免并发修改导致的数据错乱。

核心结论:ArrayList 的删除操作,只有尾部删除是真正的 O (1),其余位置的删除都需要移动元素,耗时随数据量增加而飙升。

四、核心机制:ArrayList 的扩容逻辑(源码精读,避坑关键)

扩容机制是 ArrayList 的核心灵魂,也是最容易被忽略的性能点 —— 频繁扩容会导致大量的数组拷贝和内存申请,严重影响性能。理解扩容逻辑,才能从根源上避免相关坑点。

1. 扩容的核心触发条件

当执行 add 系列方法时,会先调用 ensureCapacityInternal(size + 1) 校验容量,触发扩容的唯一条件

size + 1 > elementData.length

即「添加新元素后,实际元素个数会超过数组当前容量」,此时必须扩容,否则会触发数组下标越界异常。

2. 完整扩容流程(源码拆解,从触发到完成)

我们以无参构造的 ArrayList 首次添加元素为例,拆解从容量校验到扩容完成的完整流程,核心方法调用链:

add(E e) → 

ensureCapacityInternal(int minCapacity) → 

calculateCapacity(Object[] elementData, int minCapacity) → 

ensureExplicitCapacity(int minCapacity) → 

grow(int minCapacity)(真正的扩容方法)

步骤 1:计算最小需要的容量(calculateCapacity)

核心是处理「无参构造的空数组首次扩容」的特殊逻辑,源码:

private static int calculateCapacity(Object[] elementData, int minCapacity) {    // 若数组是无参构造的默认空数组,最小容量取DEFAULT_CAPACITY(10)和minCapacity的最大值    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {        return Math.max(DEFAULT_CAPACITY, minCapacity);    }    return minCapacity;}

关键:无参构造的 ArrayList,首次添加元素时,minCapacity 是 1,会被修正为 10,这就是「默认初始容量为 10」的底层逻辑。

步骤 2:判断是否需要扩容(ensureExplicitCapacity)

privatevoidensureExplicitCapacity(int minCapacity) {    modCount++; // 结构修改次数+1    // 最小需要的容量 > 数组当前容量 → 触发扩容    if (minCapacity - elementData.length > 0)        grow(minCapacity);}

步骤 3:真正的扩容实现(grow)—— ArrayList 扩容的核心

这是扩容的最终执行方法,包含新容量计算数组拷贝两个核心步骤,源码(JDK8 精简版):

privatevoidgrow(int minCapacity) {    // 旧容量:数组当前的容量    int oldCapacity = elementData.length;    // 核心:新容量 = 旧容量 + 旧容量/2 → 即扩容1.5倍(位运算更高效,oldCapacity >> 1 等价于 oldCapacity/2)    int newCapacity = oldCapacity + (oldCapacity >> 1);    // 处理特殊情况:新容量 < 最小需要的容量(如旧容量为0时)    if (newCapacity - minCapacity < 0)        newCapacity = minCapacity;    // 处理超大容量:新容量超过Integer.MAX_VALUE - 8,则扩容为Integer.MAX_VALUE    if (newCapacity - MAX_ARRAY_SIZE > 0)        newCapacity = hugeCapacity(minCapacity);    // 核心:数组拷贝——创建新容量的数组,将旧数组元素拷贝到新数组    elementData = Arrays.copyOf(elementData, newCapacity);}// 处理超大容量的情况,避免数组容量超过JVM的限制privatestaticinthugeCapacity(int minCapacity) {    if (minCapacity < 0// 溢出        throw new OutOfMemoryError();    return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE;}

扩容核心结论(必须记住)

  1. 默认扩容倍数
    1.5 倍(通过位运算 oldCapacity >> 1 实现,比直接除法更高效);
  2. 首次扩容特殊
    无参构造的 ArrayList,首次添加元素时,空数组会直接扩容为容量 10
  3. 扩容本质
    创建新的更大容量的数组,并通过 Arrays.copyOf 完成旧数组元素到新数组的拷贝,这个过程是O (n) 时间复杂度,且会申请新的内存空间;
  4. 超大容量限制
    数组最大容量为 Integer.MAX_VALUE(约 21 亿),避免超出 JVM 的内存管理限制。

3. 手动扩容优化:ensureCapacity (int minCapacity)

既然自动扩容有 O (n) 开销,那么在已知元素数量的场景下,我们可以手动调用 ensureCapacity 方法,提前扩容到指定容量,避免多次自动扩容的开销:

// 手动扩容方法:直接指定数组的最小容量publicvoidensureCapacity(int minCapacity) {    int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)        ? 0        : DEFAULT_CAPACITY;    if (minCapacity > minExpand) {        ensureExplicitCapacity(minCapacity);    }}

使用场景:比如要往 ArrayList 中添加 10000 个元素,无参构造的 ArrayList 会经历多次自动扩容(10→15→22→33→…→10000+),每次都有数组拷贝开销;而提前调用 list.ensureCapacity(10000),仅需一次扩容,大幅提升性能。

五、实战避坑:ArrayList 的 5 个高频错误用法(90% 的人踩过)

基于源码和底层原理,我们梳理了 ArrayList 最容易踩的 5 个坑,每个坑都给出错误写法正确解决方案,从根源上避坑。

坑点 1:无参构造后存储大量数据,导致频繁自动扩容

错误场景:创建无参构造的 ArrayList,直接添加 10 万 + 元素,触发多次自动扩容(10→15→22→…),每次都做数组拷贝,性能极差。错误写法

// 错误:无参构造,频繁自动扩容List<String> list = new ArrayList<>();for (int i = 0; i < 100000; i++) {    list.add("data" + i);}

正确解决方案提前指定初始容量,匹配实际元素数量,避免多次扩容:

// 正确:指定初始容量,仅一次扩容(若需要)List<String> list = new ArrayList<>(100000);for (int i = 0; i < 100000; i++) {    list.add("data" + i);}

进阶优化:如果元素数量不确定,可指定「预估容量 + 20%」,预留一定的冗余空间,避免少量新增元素再次触发扩容。

坑点 2:频繁做中间 / 头部增删操作,忽略元素移动开销

错误场景:用 ArrayList 实现「消息队列」「栈」等需要高频头尾 / 中间增删的场景,导致大量元素移动,性能瓶颈。错误写法

// 错误:ArrayList 头部插入,每次移动全部元素List<String> msgQueue = new ArrayList<>();// 高频头部插入新消息msgQueue.add(0"新消息1");msgQueue.add(0"新消息2");

正确解决方案:根据场景选择合适的集合:

  • 高频头尾增删:用 LinkedList(O (1))或 ArrayDeque(底层数组,头尾操作 O (1),效率比 LinkedList 更高);
  • 必须用 ArrayList:尽量将增删操作放在尾部,避免中间 / 头部操作。

坑点 3:遍历中直接调用 remove/add 方法,触发并发修改异常

错误场景:用普通 for 循环 / 增强 for 循环遍历 ArrayList 时,直接调用 remove/add 方法,修改 modCount 导致迭代器校验失败,抛出 ConcurrentModificationException错误写法

// 错误:增强 for 循环遍历中删除元素,触发并发修改异常List<String> list = new ArrayList<>(Arrays.asList("a""b""c""d"));for (String s : list) {    if ("b".equals(s)) {        list.remove(s);    }}

正确解决方案

  1. 用迭代器遍历并操作
    (推荐):迭代器的 remove/add 方法会同步更新 modCount 和 expectedModCount,避免异常;
// 正确:迭代器遍历中安全删除ListIterator<String> iterator = list.listIterator();while (iterator.hasNext()) {    String s = iterator.next();    if ("b".equals(s)) {        iterator.remove();    }}
2. 倒序 for 循环遍历
    从后往前遍历,删除元素后不会影响前面元素的下标,避免漏遍历;
3. JDK8+ 流式操作
    用 removeIf 方法,底层是迭代器实现,安全高效。

坑点 4:误以为 ArrayList 是线程安全的,多线程环境下直接使用

错误场景:多线程同时对 ArrayList 进行增删查操作,导致数据错乱、数组下标越界、空指针异常——ArrayList 是非线程安全的,底层数组和 size 均无同步机制。错误写法

// 错误:多线程同时操作 ArrayList,存在线程安全问题List<String> list = new ArrayList<>();new Thread(() -> {    for (int i = 0; i < 1000; i++) {        list.add("thread1-" + i);    }}).start();new Thread(() -> {    for (int i = 0; i < 1000; i++) {        list.add("thread2-" + i);    }}).start();

正确解决方案

  1. 用线程安全集合
    Collections.synchronizedList(new ArrayList<>())(通过同步锁实现,简单但性能一般);
  2. 高并发场景
    用 CopyOnWriteArrayList(写时复制,读多写少场景性能极佳,底层也是动态数组);
  3. 手动加锁
    在增删查操作外层加 synchronized 锁,适合自定义同步逻辑的场景。

坑点 5:用 ArrayList 存储超大对象,忽略冗余空间的内存浪费

错误场景:用 ArrayList 存储百万级超大对象(如每个对象占 1MB),由于 ArrayList 扩容为 1.5 倍,会产生大量的冗余空间(比如实际存储 100 万元素,数组容量 150 万,冗余 50 万位置),导致堆内存严重浪费正确解决方案

  1. 手动缩容
    调用 trimToSize() 方法,将数组容量缩为实际元素个数 size,释放冗余内存:
    list.trimToSize(); // 数组容量 = size,无冗余空间
  2. 选择合适的集合
   存储超大对象且元素数量固定时,直接用普通数组(无冗余空间),或用 LinkedList(按需创建节点,无提前内存申请)。

六、对比总结:ArrayList 与 LinkedList 终极选型指南

上一篇我们详细对比了二者的差异,这里结合 ArrayList 的源码细节,给出更精准、更落地的终极选型指南,附场景化选型建议,告别无脑选择。

1. 核心差异终极对比表(补充扩容、线程安全等细节)

对比维度
ArrayList(动态数组)
LinkedList(双向链表)
底层结构
动态 Object 数组,支持扩容
双向链表(Node 节点 + prev/next 指针)
随机访问
O (1)(下标直接访问,实现 RandomAccess)
O (n)(必须遍历链表)
插入 / 删除
尾部 O (1)(扩容时 O (n)),中间 / 头部 O (n)(元素移动)
头尾 O (1),中间 O (n)(遍历找节点)
扩容机制
自动扩容 1.5 倍,需数组拷贝(O (n) 隐性开销)
无扩容,按需创建节点(无拷贝开销)
内存占用
数组冗余空间(容量 > size),无额外指针开销
无冗余空间,但每个节点有 prev/next 指针(额外内存开销)
遍历效率
下标遍历 O (1),迭代器遍历 O (n),缓存命中率高
仅迭代器遍历 O (n),缓存命中率低(节点内存不连续)
线程安全
非线程安全
非线程安全
核心优势
随机查询、尾部增删、连续遍历、大数据量查询
高频头尾增删、无扩容开销、中间增删大数据量下更优

2. 场景化终极选型口诀 + 建议

核心选型口诀

查多增删少,尾部操作多 → 选ArrayList头尾增删多,中间操作少 → 选LinkedList

精细化场景建议

  1. 优先选 ArrayList 的场景(90% 的业务场景):

    • 数据展示、列表查询、分页加载(高频随机查询);
    • 仅尾部增删的场景(如日志收集、数据缓存);
    • 已知元素数量,可提前指定初始容量(避免扩容);
    • 需要高效遍历的场景(下标遍历效率拉满)。
  2. 必须选 LinkedList 的场景:

    • 实现栈、队列、双端队列(Deque)(高频头尾增删);
    • 中间增删操作频繁,且数据量较大(元素移动开销 > 链表遍历开销);
    • 内存敏感场景,避免数组冗余空间(但需接受指针的额外开销)。
  3. 进阶替代方案

    • 高并发读多写少 → 用 CopyOnWriteArrayList(ArrayList 线程安全版);
    • 高频头尾增删,追求更高效率 → 用 ArrayDeque(底层循环数组,头尾操作 O (1),比 LinkedList 更快);
    • 固定长度数据存储 → 用普通数组(无扩容、无冗余、效率最高);
    • 海量数据存储 → 用 Vector(不推荐,性能差)或第三方集合(如 Guava 的 ImmutableList)。

七、写在最后:理解底层,才能用好工具

ArrayList 看似简单,实则是 Java 集合框架中最能体现数据结构思想的实现类 —— 它的所有特性,都源于「数组」这个基础数据结构的固有属性:

  • 数组的下标随机访问,造就了 ArrayList 的查询优势;
  • 数组的固定长度,催生了动态扩容机制,也带来了拷贝开销;
  • 数组的连续内存存储,让中间增删必须移动元素,成为天生短板。

而 LinkedList 则是「链表」数据结构的完美落地,二者的差异,本质是数组与链表的经典对决

作为开发者,我们无需死记硬背「谁快谁慢」,更重要的是理解底层数据结构的特性,根据业务场景选择合适的工具 —— 这也是我们拆解源码的核心目的:知其然,更知其所以然,让每一次技术选择都有底层逻辑支撑。

下一篇预告

这篇我们拆解了 ArrayList 的动态数组结构、扩容机制和实战坑点,搞懂了它「查快插慢」的本质。下一篇,我们将进入Java 并发集合的领域,拆解 《CopyOnWriteArrayList 源码解析:写时复制,如何实现线程安全且高效读?》—— 彻底搞懂写时复制的核心原理,以及它在高并发场景下的应用。

评论区聊聊:你在项目中踩过 ArrayList 的哪些坑?是怎么解决的? 👇点赞 + 收藏,关注我们,后续干货不错过!

本站文章均为手工撰写未经允许谢绝转载:夜雨聆风 » ArrayList 源码解析:动态数组 + 扩容机制,为什么查快插慢?

评论 抢沙发

6 + 7 =
  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址
×
订阅图标按钮