【Java集合】ArrayList从入门到精通:原理、源码、扩容机制一文讲透

前言

ArrayList是Java开发中使用频率最高的集合类之一,几乎每个Java程序员每天都会与它打交道。然而,你真的了解ArrayList吗?你知道它是如何扩容的吗?你清楚remove()方法和clear()方法的区别吗?本文将从底层原理到源码实现,全方位深度解析ArrayList,让你从「会用」进化到「精通」。

一、ArrayList的核心特性

1.1 ArrayList的本质

ArrayList的底层数据结构是一个动态数组(Dynamic Array),这意味着它在内存中是连续存储的。与数组相比,ArrayList的优势在于可以自动扩容,不需要开发者在创建时指定固定大小。

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 = {};
    
    // 存储元素的数组
    transient Object[] elementData;
    
    // 集合实际大小
    private int size;
}

从源码可以看出,ArrayList内部维护了一个Object[] elementData数组来存储元素,这就是ArrayList能够随机访问的基础。

1.2 ArrayList与数组的区别

特性 Array ArrayList
大小固定 创建时确定,不可变 动态扩容,自动调整
类型安全 编译期确定 泛型支持,类型安全
性能 插入删除O(n),随机访问O(1) 同数组
灵活性
存储类型 基本类型和对象 仅对象类型

需要特别注意的是,ArrayList不能存储基本数据类型(如int、char),必须使用其包装类(如Integer、Character)。

1.3 ArrayList的继承体系

                    ┌─────────────────┐
                    │    Iterable      │
                    └────────┬────────┘
                             │
                    ┌────────▼────────┐
                    │  Collection<E>  │
                    └────────┬────────┘
                             │
                    ┌────────▼────────┘
                    │     List<E>     │
                    └────────┬────────┘
                             │
                    ┌────────▼────────┐
                    │AbstractList<E>  │
                    └────────┬────────┘
                             │
                    ┌────────▼────────┐
                    │  ArrayList<E>   │
                    └─────────────────┘

ArrayList实现了RandomAccess接口,这是一个标记接口,表示ArrayList支持快速随机访问。这意味着通过索引访问元素的时间复杂度是O(1),这是数组的特性决定的。

二、ArrayList常用方法深度解析

2.1 add()方法:添加元素的完整流程

ArrayList提供了多个add方法,我们逐个分析:

单元素添加

public boolean add(E e) {
    // 确保容量足够,如果不够则扩容
    ensureCapacityInternal(size + 1);  
    // 将元素放入数组,并移动指针
    elementData[size++] = e;
    return true;
}

指定位置插入

public void add(int index, E element) {
    // 检查索引合法性
    rangeCheckForAdd(index);
    // 确保容量足够
    ensureCapacityInternal(size + 1);
    // 将index位置及之后的元素向后移动一位
    System.arraycopy(elementData, index, elementData, index + 1, size - index);
    // 插入新元素
    elementData[index] = element;
    size++;
}

从源码可以看出,add(int index, E element)方法由于需要移动元素,其时间复杂度是O(n)。而add(E e)方法直接追加到末尾,如果没有触发扩容,时间复杂度是O(1)。

2.2 get()方法:随机访问的实现

public E get(int index) {
    // 检查索引合法性
    rangeCheck(index);
    // 直接通过数组下标访问
    return elementData(index);
}

E elementData(int index) {
    return (E) elementData[index];
}

这就是ArrayList支持快速随机访问的秘密——通过数组下标直接访问元素,时间复杂度恒为O(1)。

2.3 remove()方法:删除元素的细节

按索引删除

public E remove(int index) {
    rangeCheck(index);
    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;
}

按元素删除

public boolean remove(Object o) {
    if (o == null) {
        // 处理null元素
        for (int index = 0; index < size; index++) {
            if (elementData[index] == null) {
                fastRemove(index);
                return true;
            }
        }
    } else {
        // 遍历查找元素
        for (int index = 0; index < size; index++) {
            if (o.equals(elementData[index])) {
                fastRemove(index);
                return true;
            }
        }
    }
    return false;
}

private void fastRemove(int index) {
    modCount++;
    int numMoved = size - index - 1;
    if (numMoved > 0) {
        System.arraycopy(elementData, index + 1, elementData, index, numMoved);
    }
    elementData[--size] = null;
}

2.4 clear()方法:清空集合的差异

public void clear() {
    modCount++;
    // 将所有元素置为null,帮助GC
    for (int i = 0; i < size; i++) {
        elementData[i] = null;
    }
    size = 0;
}

注意:clear()方法只是将数组中的元素置为null,并不释放底层数组的内存空间。如果希望释放内存,可以在clear()后将ArrayList置为null。

2.5 trimToSize()方法:内存优化

public void trimToSize() {
    if (size < elementData.length) {
        elementData = Arrays.copyOf(elementData, size);
    }
}

这个方法非常实用!如果ArrayList中存储的元素远少于数组容量,调用trimToSize()可以释放多余的内存空间,使数组大小与实际元素数量一致。

三、ArrayList扩容机制深度剖析

3.1 扩容的核心方法

ArrayList的扩容机制是其最核心、最精彩的设计之一。让我们详细分析:

private void ensureCapacityInternal(int minCapacity) {
    // 判断是否是第一次添加元素
    if (elementData == EMPTY_ELEMENTDATA) {
        // 设置最小容量为默认值10和所需容量的较大值
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    // 确保容量足够
    ensureExplicitCapacity(minCapacity);
}

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

private void grow(int minCapacity) {
    // 记录原数组长度
    int oldCapacity = elementData.length;
    // 计算新容量:原容量的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);
}

3.2 扩容流程图解

初始状态:ArrayList容量为10,已存储5个元素

┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐
│ 1 │ 2 │ 3 │ 4 │ 5 │null│null│null│null│null│
└───┴───┴───┴───┴───┴───┴───┴───┴───┴───┘
          ↑
        size=5

现在要添加第6个元素,需要扩容:

第一步:计算新容量
  oldCapacity = 10
  newCapacity = 10 + (10 >> 1) = 10 + 5 = 15

第二步:创建新数组
┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐
│ 1 │ 2 │ 3 │ 4 │ 5 │ 6 │null│null│null│null│null│null│null│null│null│
└───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┘
          ↑
        size=6, capacity=15

3.3 扩容策略的巧妙设计

ArrayList选择扩容50%(1.5倍)而非翻倍,这是一个经过深思熟虑的设计决策:

1. 空间与时间的平衡

  • 扩容100%(翻倍):空间浪费严重,但扩容频率低
  • 扩容50%:空间利用率更高,但扩容更频繁
  • ArrayList选择50%,是一个平衡点

2. 减少内存碎片

适度的扩容倍数有助于减少内存碎片,使内存分配更加高效。

3. 经验与实践验证

经过大量实践验证,50%的扩容倍数在大多数场景下表现最优。

3.4 初始容量的影响

我们来看一个性能对比:

场景1:不指定初始容量

ArrayList<String> list = new ArrayList<>();
for (int i = 0; i < 100; i++) {
    list.add("item" + i);
}

执行过程:空数组 → 容量10 → 容量15 → 容量22 → … → 容量100+

共经历多次扩容,效率较低。

场景2:指定合适的初始容量

ArrayList<String> list = new ArrayList<>(100);
for (int i = 0; i < 100; i++) {
    list.add("item" + i);
}

执行过程:直接创建容量为100的数组,无扩容。

性能建议:如果能够预估集合大小,强烈建议在创建ArrayList时指定初始容量,避免频繁扩容带来的性能开销。

四、ArrayList的Fail-Fast机制

4.1 modCount的作用

ArrayList中有一个modCount字段,它记录了集合被修改的次数:

protected transient int modCount = 0;

每次调用add()remove()clear()等修改集合结构的方法时,modCount都会增加。

4.2 迭代器的Fail-Fast检测

private class Itr implements Iterator<E> {
    int cursor;       // 下一个元素的索引
    int lastRet = -1; // 上一个返回元素的索引
    int expectedModCount = modCount;  // 记录创建迭代器时的modCount

    public E next() {
        checkForComodification();  // 检查modCount是否变化
        // ... 获取下一个元素
    }

    final void checkForComodification() {
        if (modCount != expectedModCount) {
            throw new ConcurrentModificationException();
        }
    }
}

4.3 Fail-Fast的意义

Fail-Fast机制的目的是在检测到并发修改时立即抛出异常,避免出现不可预期的行为:

List<String> list = new ArrayList<>(Arrays.asList("a", "b", "c"));

// 在迭代过程中修改集合
Iterator<String> iterator = list.iterator();
list.add("d");  // 修改modCount

iterator.next();  // 抛出 ConcurrentModificationException

这种机制虽然不能完全解决并发修改问题,但能够在开发阶段帮助我们发现潜在的问题。

4.4 如何安全地遍历和删除

错误写法

for (String item : list) {
    if ("b".equals(item)) {
        list.remove(item);  // 可能抛出ConcurrentModificationException
    }
}

正确写法一:使用Iterator

Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
    String item = iterator.next();
    if ("b".equals(item)) {
        iterator.remove();  // 使用迭代器的remove方法
    }
}

正确写法二:使用removeIf(Java 8+)

list.removeIf("b"::equals);

正确写法三:倒序遍历

for (int i = list.size() - 1; i >= 0; i--) {
    if ("b".equals(list.get(i))) {
        list.remove(i);
    }
}

五、ArrayList与其他集合的对比

5.1 ArrayList vs LinkedList

特性 ArrayList LinkedList
底层结构 动态数组 双向链表
随机访问 O(1),支持 O(n),不支持
头部插入/删除 O(n) O(1)
尾部插入/删除 O(1)(均摊) O(1)
中间插入/删除 O(n) O(n),但不需要移动元素
内存占用 连续内存,可能产生碎片 每个节点需要额外空间存储指针
缓存友好

选择建议

  • 需要频繁随机访问 → ArrayList
  • 需要频繁在头部/尾部操作 → LinkedList
  • 不知道选什么 → 先选ArrayList

5.2 ArrayList vs Vector

特性 ArrayList Vector
线程安全 非线程安全 线程安全
性能 低( synchronized)
扩容策略 1.5倍 2倍(可配置)
迭代器 快速失败 安全但效率低

选择建议:如果需要线程安全的List,使用Collections.synchronizedList()包装ArrayList,或者直接使用CopyOnWriteArrayList,性能优于Vector。

5.3 ArrayList vs HashSet

特性 ArrayList HashSet
数据结构 动态数组 哈希表
元素顺序 保持插入顺序 不保证顺序
重复元素 允许 不允许
查找效率 O(n) O(1)
随机访问 支持 不支持

六、性能优化最佳实践

6.1 预分配内存

// 不推荐:每次添加都可能触发扩容
List<String> list = new ArrayList<>();

// 推荐:已知大概数量,预分配内存
List<String> list = new ArrayList<>(1000);

6.2 使用ArrayList.forEach替代for循环

// 传统写法
for (String item : list) {
    System.out.println(item);
}

// Java 8+写法,更简洁
list.forEach(System.out::println);

6.3 使用trimToSize释放内存

// 在大量数据处理完成后,释放多余内存
list.addAll(largeDataSet);
processData(list);
((ArrayList<String>) list).trimToSize();

6.4 批量操作的性能优化

// 低效:每次add都触发可能的扩容
List<String> list = new ArrayList<>();
for (String item : largeCollection) {
    list.add(item);
}

// 高效:使用addAll一次性添加
List<String> list = new ArrayList<>(largeCollection.size());
list.addAll(largeCollection);

6.5 避免在循环中调用size()

// 低效:每次循环都调用size()方法
for (int i = 0; i < list.size(); i++) {
    // 操作
}

// 高效:缓存size
int size = list.size();
for (int i = 0; i < size; i++) {
    // 操作
}

七、常见面试题解答

7.1 ArrayList的默认初始容量是多少?

ArrayList的默认初始容量是10,但只有在第一次添加元素时才会真正分配10个空间的数组。如果使用new ArrayList<>()创建空集合,实际初始容量是0。

7.2 ArrayList的扩容机制是什么?

当容量不足时,ArrayList会创建一个原容量1.5倍的新数组,然后使用Arrays.copyOf()将原数组元素复制到新数组中。这是均摊O(1)复杂度的关键。

7.3 ArrayList是线程安全的吗?

ArrayList不是线程安全的。在多线程环境下,可能出现ConcurrentModificationException或数据丢失问题。如需线程安全,推荐使用CopyOnWriteArrayList或使用Collections.synchronizedList()包装。

7.4 ArrayList的remove和clear有什么区别?

  • remove(int index):删除指定位置的元素,返回被删除的元素
  • remove(Object o):删除第一个匹配的元素,返回是否删除成功
  • clear():删除所有元素,将size置为0,但保留原数组容量

7.5 ArrayList如何实现随机访问?

ArrayList底层是数组实现,通过索引直接访问数组元素,时间复杂度O(1)。这也是它实现RandomAccess接口的原因。

八、总结

ArrayList作为Java中最常用的集合类,其设计体现了Java语言的设计哲学:

  • 简单:基于数组,操作直观
  • 高效:随机访问O(1),尾部插入均摊O(1)
  • 灵活:自动扩容,动态调整
  • 安全:Fail-Fast机制,及时发现并发问题

理解ArrayList的底层原理,不仅能帮助我们写出更高性能的代码,还能为学习其他集合类打下坚实的基础。

如果本文对你有帮助,欢迎点赞、收藏、转发!
关注我,获取更多Java核心技术文章。


参考阅读

  • JDK 8源码:java.util.ArrayList
  • 《Effective Java》第三版 - 第50条:优先考虑泛型
  • Java官方文档:ArrayList API文档
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐