深入剖析Java ArrayList源码

一、引言

1.为什么选择ArrayList?

这里我想从两个方面分析,一是它是在什么场景下使用,二是它比数组的优势在哪里,下面我们一个个分析

1.1开发中的高频使用场景

ArrayList作为Java集合框架中最常用的动态数组实现,其高频使用场景与其设计特性密切相关。以下是实际开发中最典型的应用场景:

(1) 数据动态增长需求 场景示例: 用户注册时批量导入数据、日志系统收集动态生成的日志条目、电商购物车商品数量变化等。

// 数据动态增长示例
List logs = new ArrayList<>();
while (系统运行中) {
    String log = 生成日志();
    logs.add(log); // 无需关心底层数组扩容
}

核心优势: ArrayList 的自动扩容机制(grow()方法)让开发者无需预先计算数据规模,规避了传统数组的定长限制。即使预估错误,也不会导致
ArrayIndexOutOfBoundsException。

(2) 临时数据容器 场景示例:解析 CSV/JSON 数据、网络请求结果反序列化、数据库查询结果集映射。

// JSON解析示例
List<Map> tempList = new ArrayList<>();
while (jsonParser.hasNext()) {
    tempList.add(parseItem(jsonParser));
}
// 使用后快速释放
tempList.clear(); // 清空但不回收数组,复用内存空间

内存管理优势: ArrayList的clear()方法仅将size置零,保留底层数组(elementData)避免频繁 GC。适合短期存活对象,减少内存分配开销。

1.2对比数组的灵活性优势

通过一个表格详细对比ArrayList与原生数组的差异

对比维度

原生数值

ArrayList

初始化

必须显式指定长度(int[] arr = new int[10])

支持懒加载(默认构造器初始化为空数组)

扩容机制

需手动创建新数组并拷贝数据

自动按1.5倍扩容(grow()方法)

内存管理

扩容后旧数组成为垃圾对象

内部elementData数组复用,减少GC压力

代码对比:

// 原生数组扩容
int[] arr = new int[10];
int[] newArr = new int[arr.length * 2];
System.arraycopy(arr, 0, newArr, 0, arr.length);
arr = newArr;

// ArrayList扩容(完全封装,用户无感知)
List list = new ArrayList<>();
list.add(1); // 首次添加触发扩容至10

二、ArrayList核心设计

1.类结构解剖

我们先看ArrayList的类结构继承图

2.核心接口介绍

**2.1 List接口的实现**

作为动态数组的核心契约,List 接口的实现是 ArrayList 的基础能力来源。

(1) 动态扩容的 add 方法

// 添加单个元素(核心方法)
public boolean add(E e) {
    modCount++; // 结构性修改计数器
    add(e, elementData, size); // 内部方法
    return true;
}

private void add(E e, Object[] elementData, int s) {
    if (s == elementData.length) // 容量已满
        elementData = grow();     // 扩容逻辑
    elementData[s] = e;          // 尾部插入
    size = s + 1;                // 更新逻辑大小
}

设计特点:

自动扩容:通过 grow() 方法实现 1.5 倍扩容(int newCapacity = oldCapacity + (oldCapacity >> 1)),平衡内存占用和扩容频率。

尾部插入优化:时间复杂度为 O(1)(不扩容时),相比 LinkedList 的中间插入更高效。

(2) 随机访问的 get 方法

public E get(int index) {
    Objects.checkIndex(index, size); // JDK9+ 边界检查
    return elementData(index);       // 直接数组访问
}

// 无检查的底层访问(包级私有)
E elementData(int index) {
    return (E) elementData[index];
}

性能优势:

直接通过数组下标访问,时间复杂度为 O(1),是 RandomAccess 接口的典型实现。

对比 LinkedList 的 get(int index)(需要遍历链表,时间复杂度 O(n))。

2.2 Clone接口实现

ArrayList 实现了浅拷贝(Shallow Copy)的克隆机制。

(1) clone() 方法源码

public Object clone() {
    try {
        ArrayList v = (ArrayList) super.clone();
        v.elementData = Arrays.copyOf(elementData, size); // 复制数组
        v.modCount = 0; // 重置修改计数器
        return v;
    } catch (CloneNotSupportedException e) {
        throw new InternalError(e); // 理论上不会发生
    }
}

关键点:

浅拷贝:仅复制对象引用,不复制元素对象本身。

数组拷贝:Arrays.copyOf 复制原数组的有效部分(size 长度),避免保留多余容量。

三、 ArrayList的几个构造函数详解

3.1 默认构造器:懒加载策略与10容量奥秘

源码实现

// JDK 21 源码
public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

由此可以看出默认构造函数在初始化的时候被复制为空数组,不立即分配内存,这属于懒加载机制, 在首次调用 add() 时触发扩容,容量设为默认值 10:代码如下:

private void add(E e, Object[] elementData, int s) {
    if (s == elementData.length)
        elementData = grow(); // 首次添加时扩容
    elementData[s] = e;
    size = s + 1;
}

private Object[] grow() {
    return grow(size + 1); // 最小容量为当前size+1
}

private Object[] grow(int minCapacity) {
    int oldCapacity = elementData.length;
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        // 首次扩容时使用默认容量10
        return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)];
    }
}

适用场景:

临时存储:不确定初始数据量且可能长期空置的列表。

内存敏感环境:避免提前占用未使用的内存空间。

潜在风险:

多次扩容代价:若最终存储大量数据,频繁扩容可能导致性能损耗(如添加1000个元素需扩容 7次)。

3.2 指定容量构造器:精准内存控制

这个代码如下:

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);
    }
}

可以看出根据调用者传递的容量初始化Object数组的elementData长度。这个构造函数有什么优点那?

零扩容开销: 若预设容量 ≥ 最终数据量,添加操作的时间复杂度稳定为 O(n)。

空间最优化: 无未使用槽位,内存利用率 100%。

适用场景:

大数据处理:已知数据规模(如分页查询每页固定100条)。

高频写入:实时日志收集等需要避免扩容延迟的场景。

代码示例

// 预分配容量避免扩容
List logs = new ArrayList<>(10000);
while (logSource.hasNext()) {
    logs.add(parseLog(logSource.next())); // 无扩容开销
}

3.3 集合参数构造器:安全数据迁移

这个有参构造函数的源码如下所示:

public ArrayList(Collection c) {
    Object[] a = c.toArray();
    if ((size = a.length) != 0) {
        if (c.getClass() == ArrayList.class) {
            elementData = a; // 直接复用数组(前提是原始集合为ArrayList)
        } else {
            elementData = Arrays.copyOf(a, size, Object[].class);
        }
    } else {
        elementData = EMPTY_ELEMENTDATA;
    }
}

关键设计:

高效复制:

若输入集合是 ArrayList,直接复用其底层数组(无拷贝开销)。

若非 ArrayList,通过 Arrays.copyOf 深度拷贝,时间复杂度 O(n)。

空集合处理:若输入集合为空,初始化 elementData 为空数组。

安全机制:

数据隔离:通过拷贝操作确保新列表与原集合无引用关联,避免意外修改。

类型安全:Arrays.copyOf 自动处理泛型数组类型转换问题。

适用场景:

集合转换:将其他集合类型(如 LinkedList、Set)转换为动态数组。

防御性拷贝:需要独立副本以避免共享数据修改的场景。

3.4 三种构造函数的对比

通过上面分析可以看出三种构造函数的各自设计特点以及适用场景,下面画个图表总结一下它们。

维度

默认构造器

指定容量构造器

集合参数构造器

内存分配时机

首次添加元素时(懒加载)

初始化时立即分配

初始化时根据输入集合大小分配

初始容量

0(首次扩容到10)

用户指定

等于输入集合的 size

适用场景

不确定数据量的临时存储

已知数据规模的预分配

集合转换

扩容开销

可能多次扩容(最差O(log n)次)

无(若容量足够)

无(容量精确匹配集合大小)

线程安全性

非线程安全

非线程安全

非线程安全

四、核心方法源码解析

1. ArrayList的add方法源码分析

首先我们看该方法的签名,我用的是jdk21:

public boolean add(E e) {
    modCount++;
    add(e, elementData, size);
    return true;
}

private void add(E e, Object[] elementData, int s) {
        if (s == elementData.length)
        elementData = grow();
        elementData[s] = e;
        size = s + 1;
}
private Object[] grow() {
    return grow(size + 1);
}
private Object[] grow(int minCapacity) {
    int oldCapacity = elementData.length;
    if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        int newCapacity = ArraysSupport.newLength(oldCapacity,
        minCapacity - oldCapacity, /* minimum growth */
        oldCapacity >> 1           /* preferred growth */);
        return elementData = Arrays.copyOf(elementData, newCapacity);
    } 
    else {
    return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)];
    }
}

public static int newLength(int oldLength, int minGrowth, int prefGrowth) {
       
    int prefLength = oldLength + Math.max(minGrowth, prefGrowth); 
    if (0 < prefLength && prefLength <= SOFT_MAX_ARRAY_LENGTH) {
    return prefLength;
    } else {
    // put code cold in a separate method
    return hugeLength(oldLength, minGrowth);
    }
}

private static int hugeLength(int oldLength, int minGrowth) {
    int minLength = oldLength + minGrowth;
    if (minLength < 0) {
        throw new OutOfMemoryError(
        "Required array length " + oldLength + " + " + minGrowth + " is too large");
    } 
    else if (minLength <= SOFT_MAX_ARRAY_LENGTH) {
      return SOFT_MAX_ARRAY_LENGTH;
    } else {
    return minLength;
    }
}

从add方法开始调用了重载方法,之后又调用了grow的重载方法,我一步步分析。 首先把modCount值自增加1,它记录了结构性修改的次数,主要是实现Fail-Fast机制,主要是迭代遍历时候防止被其他线程修改,如果修改抛出
ConcurrentModificationException,接着调用add方法,传入要添加的元素e,Object类型数组elementData,s代表集合中元素个数,首次添加时候是 0,第一次添加由于Object数组elementData的长度是0,前面也说了属于惰性的思想,会调用grow方法,grow又调用了重载还是把minCapacity的值设为1传入,把数组长度赋值 给oldCapacity,判断它是否大于0或者elementData不是空数组,很显然第一次添加不满足这个条件走到else语句,初始化数组长度是10,然后grow方法执行结束返回到add 方法,此时elementData数组下标0的元素是e,size的值1,执行结束。

由于数组初始化长度是10,当添加第十一个元素时候又走到grow(int minCapactiy)方法,此时判断oldCapacity是否大于0, 很显然是大于0,走到if语句中,此次调用了ArraysSupport.newLength方法,传入参数是oldCapacity=10,minCapacity - oldCapacity=11-10, oldCapacity >> 1=10 >> 1,右移1位,得到的值是5,执行int prefLength = oldLength + Math.max(minGrowth, prefGrowth); 这里面的逻辑是新容量优先选择旧容量 + 首选增长量但如果首选增长量 < 最小增长量,则选择旧容量 + 最小增长量。如果prefLength小于Integer的最大长度-8返回 prefLength的值,若计算值超过SOFT_MAX_ARRAY_LENGTH,调用hugeLength方法,判断minLength<0,说明整数溢出抛出错误异常, 若必须扩容到 minLength 超过SOFT_MAX_ARRAY_LENGTH,则允许扩容到 Integer.MAX_VALUE。得到最终容量是之前的1.5倍,即新数组的容量是15, 调用Arrays.copyOf(elementData, newCapacity)方法复制数组,把elementData引用指向复制后的新数组,之前的旧的数组会被 GC垃圾收集器收集。这里有个问题需要思考一下为什么要扩容到1.5倍那,查阅相关资料得到这样的答案:

ArrayList选择1.5倍扩容是为了在性能(减少扩容次数)和内存利用率(避免过多浪费)之间取得平衡。这使得ArrayList在大部分应用场景下能提供良好的性能和较低的内存开销。

最后给大家出个ArrayList的add方法一个执行流程图方便理解:

2. ArrayList的get方法源码分析

get方法比较简单就是取得给定下标在数组中的指定位置,相信大家都能看懂,我还是给出get方法的签名:

@Override
public E get(int index) {
    return a[index];
}

3. ArrayList的contains方法源码分析

contains方法的签名如下所示:

public boolean contains(Object o) {
    return indexOf(o) >= 0;
}

public int indexOf(Object o) {
    return indexOfRange(o, 0, size);
}

int indexOfRange(Object o, int start, int end) {
    Object[] es = elementData;
    if (o == null) {
        for (int i = start; i < end; i++) {
            if (es[i] == null) {
            return i;
            }
        }
    } else {
        for (int i = start; i < end; i++) {
            if (o.equals(es[i])) {
            return i;
            }
        }
    }
    return -1;
}

可以看出传入Object类型的对象,调用indexOf方法最终调用了indexOfRange方法,入参有Object类型 o,起始位置int类型start=0,结束位置是int类型容器中元素的个数end=size, 把elementData数组赋值给一个临时变量es,判断要包含的元素是否为null,如果为null,遍历从start-end下标值如果是null的返回下标值。

如果不是null,调用equals方法比较对象是否相同,如果相等返回下标,如果不相等返回-1,根据返回下标判断是否大于等于0,如果是返回true,代表包含,否则不包含,这里需要注意一点如果是自定义的引用类型应该重写equals方法,保证符合业务逻辑,只要内容相同就代表包含这个对象。

4. ArrayList的remove方法源码分析 remove方法有两个重载的方法,一个参数类型是int的,代表根据下标删除对象,一个参数类型是Object的,代表根据对象删除,我们一个个来看,首先看根据下标删除的方法签名

public E remove(int index) {
    Objects.checkIndex(index, size);
    final Object[] es = elementData;

    @SuppressWarnings("unchecked") E oldValue = (E) es[index];
    fastRemove(es, index);

    return oldValue;
}

private void fastRemove(Object[] es, int i) {
    modCount++;
    final int newSize;
    if ((newSize = size - 1) > i)
    System.arraycopy(es, i + 1, es, i, newSize - i);
    es[size = newSize] = null;
}

根据传入的下标得到数组存储的值oldValue,然后调用fastRemove方法,modCount值自增1,定义变量newSize, 把size-1赋值给newSize,并且判断是否大于传入的下标,如果大于,说明是移除中间的某个下标元素,调用System.arraycopy方法把下标i后面元素移动到要删除下标位置上,这样做的目的是使数组地址连续, 最后把newSize的值赋值给size并且把size处元素置为null最终返回oldValue值。

下面我们来看看另一个重载方法remove的参数类型是Object的,方法签名如下:

public boolean remove(Object o) {
    final Object[] es = elementData;
    final int size = this.size;
    int i = 0;
    found: {
        if (o == null) {
            for (; i < size; i++)
                if (es[i] == null)
                    break found;
        } else {
            for (; i < size; i++)
                if (o.equals(es[i]))
                    break found;
        }
        return false;
    }
    fastRemove(es, i);
    return true;
}

这个方法也比较简单,还是根据要删除对象,循环遍历比较equals方法如果有相等的就返回下标值,之后的调用和上面分析的逻辑一样,不在复述。

五、结束语

通过层层剖析ArrayList的源码,我们得以窥见Java集合框架的深邃智慧。Object[] elementData 的朴实无华,承载着动态扩容的灵活;size 字段的静默记录,维系着逻辑与物理的平衡;从默认构造器的懒加载策略到 grow()方法,每一行代码都在诉说着工程与艺术的交融。 ArrayList 的设计,是时间与空间的博弈,是安全与效率的权衡,更是开发者与机器的默契。它用最简洁的数组结构,支撑起高频查询的极致性能;以看似平凡的增删逻辑,诠释了算法优化的精妙。 然而,源码的魅力不止于理解,更在于启示。希望我们程序员都能写出高效的代码。

相关文章

.ArrayList源码三:数组扩容以及元素拷贝

数组扩容以及元素拷贝 这个 ArrayList 动态性的核心机制。ArrayList 源码三:数组扩容以及元素拷贝在之前的文章中,我们已经了解到 ArrayList 底层是基于数组 elementDa...

系列专栏(六):解构赋值

ES6作为新一代JavaScript标准,已正式与广大前端开发者见面。为了让大家对ES6的诸多新特性有更深入的了解,Mozilla Web开发者博客推出了《ES6 In Depth》系列文章。CSDN...

数组结构分为一维数组二维数组一维数组赋值使用

数组结构数组是最为常见的一种数据结构,不是基本数据类型是对象,是相同类型的用一个标识符封装到一起的基本类型数据序列组成一个对象。可以用一个统一的数组名和下标来确定数组中的元素。数组包括一维数组和二维数...

第二章 数组 2.1 数组的声明、赋值及读取

第二章 数组2.1 数组的声明、赋值及读取数组,就是多个数据或变量的集合。数组分为一维数组、二维数组、三维数组......等等。通常我们使用的是一维数组和二维数组。数组是先声明,后使用。1、声明数组数...

VBA一组代码如何搞定赋值给数组arr及回填数据给...

VBA++ 题记:一剪闲云一溪月,一程山水一年华。一世浮生一刹那,一树菩提一烟霞。岁月静好,现世安稳。纵算云水漂泊,心若安宁,亦可淡若清风。希望见者与不见者都能安康。静下心,多学习有用的知识,多提高自...

数组双指针直接秒杀七道题目

双指针技巧在处理数组和链表相关问题时经常用到,主要分为两类:左右指针和快慢指针。所谓左右指针,就是两个指针相向而行或者相背而行;而所谓快慢指针,就是两个指针同向而行,一快一慢。对于单链表来说,大部分技...