前言

ArrayList 几乎是每个 Java 程序员每天都要打交道的类。面试官最喜欢问:“它是怎么扩容的?”或者“它的初始化容量是多少?”。今天我们放下课本,直接看代码,把这个面试重灾区彻底吃透。


一、 核心成员变量:ArrayList 的骨架

打开 ArrayList 的源码,你会看到几个关键角色:

// 默认的初始容量为 10
private static final int DEFAULT_CAPACITY = 10;
​
// 用于空实例的共享空数组实例
private static final Object[] EMPTY_ELEMENTDATA = {};
​
// 存储元素的缓冲区(这就是 ArrayList 的本质:数组)
transient Object[] elementData; 
​
// 包含的元素数量
private int size;

关键点:

  • transientelementData 被标记为 transient,意味着它不会被默认的序列化机制处理。ArrayList 内部通过 writeObjectreadObject 自定义了序列化逻辑,只序列化实际有数据的部分,节省空间。

  • Object[]:这就是为什么 ArrayList 能存任何对象,但在取出来时需要类型转换(泛型在编译后会擦除)。


二、 初始化:懒加载的智慧

在 JDK 1.8 之后,当你执行 new ArrayList() 时,它并没有立即创建一个长度为 10 的数组。

public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

此时 elementData 是一个空数组。真正的容量分配发生在第一次调用 add() 方法时。这种“延时加载”的设计避免了创建大量空集合导致的内存浪费。


三、 扩容机制:核心算法解析(重点)

当我们调用 add() 时,ArrayList 会先检查容量是否足够。

1. 扩容入口

private void ensureCapacityInternal(int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        // 如果是第一次添加,取 10 和 minCapacity 的最大值
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    ensureExplicitCapacity(minCapacity);
}

2. 核心 grow 方法

当空间不足时,会触发 grow(int minCapacity) 方法:

private void grow(int minCapacity) {
    int oldCapacity = elementData.length;
    // 核心扩容逻辑:新容量 = 旧容量 + 旧容量 / 2 (即 1.5 倍)
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    
    // 检查新容量是否满足最小需求
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
        
    // 检查是否超过最大限制 (MAX_ARRAY_SIZE)
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
        
    // 拷贝原数据到新数组
    elementData = Arrays.copyOf(elementData, newCapacity);
}

面试大白话: 扩容就像是搬家。原先的房子(数组)住不下了,我们找一个 1.5 倍大的新房子,把家具(元素)全部搬过去(System.arraycopy),最后把老房子拆了(旧数组等待 GC 回收)。


四、 核心方法源码细节

1. add(E e) —— 尾部插入

public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // 确保容量
    elementData[size++] = e;          // 直接在末尾赋值
    return true;
}
  • 复杂度:平均 O(1),除非触发扩容。

2. add(int index, E element) —— 指定位置插入

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++;
}
  • 复杂度:O(n)。这就是为什么说 ArrayList 在中间插入元素效率低的原因。


五、 关于 System.arraycopy() 和 Arrays.copyOf()

在源码中,这两个方法频繁出现:

  • Arrays.copyOf():主要用于扩容。它内部会创建一个新数组,然后调用 System.arraycopy 把数据填进去。

  • System.arraycopy():这是个 native 方法(底层由 C/C++ 实现),用于在同一个数组或不同数组间移动数据。它的性能极高,远快于我们手写 for 循环。


六、 进阶思考:为什么 ArrayList 线程不安全?

add() 方法中:

  1. elementData[size++] = e; 这行代码不是原子的。

  2. 它分为两步:先在 size 处赋值,再执行 size = size + 1

并发风险: 如果线程 A 和线程 B 同时执行。A 刚写完值还没来得及 size++,B 进来了,也往同一个 size 位置写,就会导致 A 的数据被覆盖。甚至可能因为 size 还没更新,导致数组下标越界。


七、 总结:如何优雅地使用 ArrayList?

  1. 预估容量:如果知道要存 500 个元素,请用 new ArrayList<>(500)。这样可以避免约 7-8 次的扩容和数组拷贝。

  2. 谨慎删除:如果需要大量删除元素,建议从后往前删,或者直接使用 removeIf(Java 8+)。

  3. 多线程避坑:在并发环境下,请使用 CopyOnWriteArrayListCollections.synchronizedList


结语: 源码是最好的老师。通过今天的分析,相信你对 ArrayList 的扩容、性能瓶颈以及底层数组操作有了全新的认识。

如果你觉得这篇源码分析够干货,别忘了收藏转发!

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐