ArrayList 源码分析

一、前言

ArrayList 是 Java 集合框架中最常用的可变长数组实现,提供了随机访问(O(1) 时间复杂度)和按需扩容的能力。

它实现了 ListRandomAccessCloneableSerializable 等接口,拥有广泛应用场景,如缓存、队列、简单的数据聚合等。
本文是作者学习总结的文章,有错误的地方还请指出。


二、ArrayList 的类结构

继承与接口

​编辑

  • 继承自 AbstractList<E>,实现了 List<E> 接口。
  • 实现了标记接口 RandomAccess(支持快速随机访问)、Cloneable(可克隆)、Serializable(可序列化)

关键字段

arduino 复制代码
// 初始容量大小
private static final int DEFAULT_CAPACITY = 10;

// 起始空数组
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

// 底层数组缓存,容量由此决定
// transient 关键字保证数据不被序列化,后续会进行解释
transient Object[] elementData; 

// 当前实际元素个数
private int size;                
  • elementData 起始为空数组(DEFAULTCAPACITY_EMPTY_ELEMENTDATA),真正容量延迟到首次添加元素时才设为默认值 10。
  • size 记录已添加元素数,仅当 size 改变时才修改。

三、底层数据结构

ArrayList 本质上是不断扩容的数组:

  1. 初始时 elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA,容量 0。
  2. 首次 add 时,将容量设为 DEFAULT_CAPACITY = 10
  3. 后续若需求超过当前容量,则按 old + (old >> 1)(1.5 倍)扩容。

四、构造方法分析

arduino 复制代码
// 空参构造方法
ublic ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

// 指定内存容量的构造方法
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);
}

// 一次性填充集合的构造方法
public ArrayList(Collection<? extends E> c) {
    elementData = c.toArray();
    size = elementData.length;
    // 若 c.toArray() 返回类型不为 Object[],则拷贝确保类型
    if (elementData.getClass() != Object[].class)
        elementData = Arrays.copyOf(elementData, size, Object[].class);
}
  • 无参构造延迟到第一次添加元素时设默认容量 10。
  • 指定容量构造直接分配,避免不必要扩容。
  • 集合构造一次性填充,提高性能。

五、添加元素相关源码

scss 复制代码
// 添加元素到数组列表中
public boolean add(E e) {
    // 确保内部容量足够容纳新元素(size + 1)
    ensureCapacityInternal(size + 1);
    // 将元素放入数组末尾
    elementData[size++] = e;
    return true;
}

// 确保当前内部容量足够(实际入口)
private void ensureCapacityInternal(int minCapacity) {
    // 如果当前数组是空数组(默认空数组),则取默认容量与期望容量的较大值
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);

    // 显式检查容量
    ensureExplicitCapacity(minCapacity);
}

// 显式检查并扩容(若必要)
private void ensureExplicitCapacity(int minCapacity) {
    // 结构性修改次数 +1(用于快速失败机制)
    modCount++;
    // 如果需要的容量 > 当前数组容量,触发扩容
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

// 扩容逻辑
private void grow(int minCapacity) {
    // 旧容量
    int oldCapacity = elementData.length;
    // 新容量 = 旧容量 + 旧容量 / 2(即扩容1.5倍)
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    // 如果新容量还是小于所需最小容量,则直接使用所需最小容量
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    // 如果新容量超过最大数组大小限制,使用更大的容量策略
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    // 复制原数组内容到新数组,并替换引用
    elementData = Arrays.copyOf(elementData, newCapacity);
}

// 处理极大容量请求的情况
private static int hugeCapacity(int minCapacity) {
    // 如果所需容量是负数,说明发生了整数溢出,抛出内存溢出异常
    if (minCapacity < 0) // overflow
        throw new OutOfMemoryError();
    
    // 如果所需容量大于最大数组限制(Integer.MAX_VALUE - 8),
    // 则直接返回 Integer.MAX_VALUE,否则返回 MAX_ARRAY_SIZE
    return (minCapacity > MAX_ARRAY_SIZE) ?
        Integer.MAX_VALUE :
        MAX_ARRAY_SIZE;
}
  • ensureCapacityInternal 负责触发扩容,modCount 用于 Fail‑Fast 检测。
  • 扩容策略:新容量 = 旧容量 + 旧容量/2(即 1.5 倍)。
  • 扩容开销主要在 Arrays.copyOf,需谨慎避免频繁扩容。

六、获取元素与范围校验

arduino 复制代码
// 获取指定位置上的元素
public E get(int index) {
    // 检查索引是否合法(是否在 0 到 size-1 之间)
    rangeCheck(index);
    
    // 安全地返回指定索引处的元素,类型转换为泛型 E
    return (E) elementData[index];
}

// 检查访问索引是否合法的方法
private void rangeCheck(int index) {
    // 如果索引小于 0 或者大于等于当前元素个数,说明越界了
    if (index < 0 || index >= size)
        // 抛出数组越界异常,并携带提示信息
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
  • rangeCheck(index) 是为了防止访问非法索引,保护程序稳定性。
  • elementData 是一个 Object[] 类型的数组,存储实际的数据;取出时强制转换为泛型类型 E

七、删除元素源码分析

arduino 复制代码
// 根据索引移除元素
public E remove(int index) {
    // 检查索引是否合法
    rangeCheck(index);
    // 修改次数加1,用于fail-fast机制
    modCount++;
    // 保存旧值,用于返回
    E oldValue = (E) elementData[index];

    // 计算需要移动的元素个数(从 index+1 开始到最后一个元素)
    int numMoved = size - index - 1;
    // 如果有元素需要向前移动,则使用 System.arraycopy 进行数组搬移
    if (numMoved > 0)
        System.arraycopy(elementData, index + 1, elementData, index, numMoved);

    // 将最后一个元素置为 null,帮助 GC 回收
    elementData[--size] = null;

    return oldValue; // 返回被移除的元素
}

// 根据对象值移除元素
public boolean remove(Object o) {
    // 处理 null 元素的情况(== 判断)
    if (o == null) {
        for (int index = 0; index < size; index++) {
            if (elementData[index] == null) {
                fastRemove(index); // 快速移除
                return true;
            }
        }
    } else {
        // 非 null 情况,用 equals 判断是否相等
        for (int index = 0; index < size; index++) {
            if (o.equals(elementData[index])) {
                fastRemove(index); // 快速移除
                return true;
            }
        }
    }
    // 未找到元素,返回 false
    return false; 
}

// 快速移除指定索引的元素,不进行返回
private void fastRemove(int index) {
    modCount++; // 修改次数加1
    int numMoved = size - index - 1;
    // 搬移后续元素覆盖当前索引位置
    if (numMoved > 0)
        System.arraycopy(elementData, index + 1, elementData, index, numMoved);
    // 将尾部置空并更新 size
    elementData[--size] = null;
}

// 构造错误信息的方法
private String outOfBoundsMsg(int index) {
    return "Index: "+index+", Size: "+size;
}
  • remove(int index):用于删除指定索引位置的元素,并返回旧值。
  • remove(Object o):用于根据对象内容删除列表中第一个匹配的元素(支持 null 值)。
  • fastRemove(int index):简化版本的移除操作,用于内部调用,不需要返回被删除的值。
  • modCount++ 是为了支持 Java 集合的快速失败机制(fail-fast)。

八、扩容机制详解

  • 默认容量:10(第一次 add 时生效)
  • 触发条件:minCapacity > elementData.length
  • 扩容公式:newCapacity = oldCapacity + (oldCapacity >> 1)(1.5 倍),若不足以满足 minCapacity,则取 minCapacity
  • 最大容量:Integer.MAX_VALUE - 8,超过时抛 OutOfMemoryError 或调整到最大值。
  • 扩容为 O(1),大多数 add 操作只需常数时间。

九、Fail‑Fast 机制

  • ArrayList 的迭代器通过记录创建时的 expectedModCount = modCount,每次调用 next()remove() 时比对 modCount,若不一致则抛出 ConcurrentModificationException,快速失败,避免不确定行为。
  • 这种机制,在多线程环境下不保证绝对检测,但对单线程错误用法能及时暴露。

十、和其他集合的对比

对比项 ArrayList LinkedList Vector
底层结构 动态数组 双向链表 动态数组(同步)
随机访问 O(1) O(n) O(1)
头/尾插入删除 O(n) O(1) O(n)
扩容策略 1.5 倍 不需扩容 2 倍(默认)
线程安全 是(同步)
适用场景 随机访问、读多写少 频繁增删(尤其是头部) 需要线程安全但性能较低时

十一、常见面试题总结

  • ArrayList 默认容量是多少?

    • 默认为 10,但延迟到首次 add 时才分配
  • 扩容机制如何实现?

    • 新容量 = old + (old >> 1),不足时取 minCapacity,最大不超过 Integer.MAX_VALUE - 8
  • 为什么线程不安全?

    • 内部无同步,且 modCount 检测不是原子操作;多线程并发增删会导致数据不一致。
  • 如何实现线程安全的 List?

    • 使用 Collections.synchronizedList(new ArrayList<>())
    • 使用 CopyOnWriteArist(juc 包下的集合,采用写时复制的思想)
    • 使用 Vector(性能低,内部使用的 synchronized 修饰的方法)
  • Fail‑Fast 原理是什么?

    • 通过 modCount 与迭代器中的 expectedModCount 比对,检测到不匹配则抛 ConcurrentModificationException
  • 数组为什么使用 transient 进行修饰?

    • transient 的意思是:这个字段在对象被序列化时不会被序列化。

    • 不希望数组被默认序列化

      • 数组可能只用了一部分,但数组有10个元素,会把所有空位也一起序列化出去(不利于效率与资源节省)
    • 自定义序列化更灵活

      • ArrayList 使用了自定义的 writeObject()readObject() 方法,仅序列化实际有用的数据
    • 总结一句话:ArrayList 中的数组使用 transient 修饰,是为了避免无效元素被序列化,提高性能,并通过自定义序列化方法控制存储内容

相关推荐
西洼工作室2 分钟前
黑马商城-微服务笔记
java·笔记·spring·微服务
异常君3 分钟前
MySQL重复数据克星:7种高效处理方案全解析
java·后端·mysql
异常君7 分钟前
Spring 定时任务执行一次后不再触发?5 大原因与解决方案全解析
java·后端·spring
异常君9 分钟前
Java 序列化工具:@JSONField 注解实战解析与应用技巧
java·后端·json
写bug写bug42 分钟前
Java并发编程:什么是线程组?它有什么作用?
java·后端
Andya_net1 小时前
SpringBoot | 构建客户树及其关联关系的设计思路和实践Demo
java·spring boot·后端
申城异乡人1 小时前
【踩坑系列】使用Comparator.comparing对中文字符串排序结果不对
java
Brian_Lucky1 小时前
在 macOS 上合并 IntelliJ IDEA 的项目窗口
java·macos·intellij-idea
周杰伦_Jay1 小时前
continue插件实现IDEA接入本地离线部署的deepseek等大模型
java·数据结构·ide·人工智能·算法·数据挖掘·intellij-idea