ArrayList 的源码剖析:ArrayList底层的实现原理是什么

84 阅读7分钟

引言

A List is an ordered Collection (sometimes called a sequence). Lists may containduplicate elements.

列表是有序的集合(有时称为序列)。 列表可能包含重复的元素。

The Java platform contains two general-purpose List implementations. ArrayList,which is usually the better-performing implementation, and LinkedList whichoffers better performance under certain circumstances.

Java平台包含两个常用的List实现。 ArrayList通常是性能较好的实现,而LinkedList在某些情况下可 以提供更好的性能。

List 接口常用方法

E get(int index); //获取给定位置的元素
E set(int index, E element);//修改给定位置的元素
void add(int index, E element);//在给定位置插入一个元素
E remove(int index);//移除给定位置的元素
int indexOf(Object o);//获取给定元素第一次出现的下标
int lastIndexOf(Object o);//获取给定元素最后一次出现的下标
ListIterator<E> listIterator();//获取List集合专有的迭代器
ListIterator<E> listIterator(int index);//获取List集合专有的迭代器,从给定的下标位置开
始的迭代器
List<E> subList(int fromIndex, int toIndex);//获取List集合的一个子集合

ArrayList 的底层是基于数组实现的。在 ArrayList 类中,有一个核心的成员变量 elementData,它就是用来存储元素的数组。以下是 ArrayList 中部分相关源码:

// 存储元素的底层数组(transient表示序列化时不直接序列化此数组,需通过writeObject手动处理)
transient Object[] elementData; 
// 集合中实际元素的数量(区别于数组容量capacity) 
private int size; 
// 默认初始容量(无参构造器第一次添加元素时的初始容量) 
private static final int DEFAULT_CAPACITY = 10; 
// 空数组(用于用户指定初始容量为0时) 
private static final Object[] EMPTY_ELEMENTDATA = {}; 
// 空数组(用于无参构造器,与EMPTY_ELEMENTDATA区分,第一次添加元素时会扩容至DEFAULT_CAPACITY) private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
// 数组最大容量(避免OutOfMemoryError,Integer.MAX_VALUE - 8是为了预留部分内存给数组头信息) 
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

elementData 被声明为 transient,这意味着在序列化时会忽略该字段,因为 ArrayList 有自己的序列化逻辑。size 变量则记录了当前 ArrayList 中实际存储的元素数量。

1. EMPTY_ELEMENTDATA:用户指定容量为 0 时的 "空容器"

  • 场景:当你主动创建 ArrayList 时指定初始容量为 0(比如new ArrayList(0)),就会用到这个空数组。
  • 特点:它是一个 "纯粹的空数组",第一次添加元素时不会默认扩容到 10,而是根据添加的元素数量直接扩容(比如添加 1 个元素就扩容到 1)。
  • 举例
    你明确告诉 ArrayList:"我一开始就只要 0 个容量",它就用EMPTY_ELEMENTDATA,后续添加元素时完全按你的需求来分配空间。

2. DEFAULTCAPACITY_EMPTY_ELEMENTDATA:无参构造器的 "初始空容器"

  • 场景:当你用无参构造器创建 ArrayList(new ArrayList())时,默认会用这个空数组。
  • 特点:它是一个 "带默认扩容约定的空数组",第一次添加元素时会自动扩容到 10(默认初始容量)。
  • 举例
    你没告诉 ArrayList 初始要多大,它就先用DEFAULTCAPACITY_EMPTY_ELEMENTDATA占个位置,等你第一次加元素时,自动帮你把容量设为 10(避免频繁扩容)。

为什么要区分两个空数组?

本质是为了处理不同初始化场景的扩容逻辑

  • 如果你主动指定容量为 0,说明你可能清楚后续元素数量不多,ArrayList 就 "严格按你说的来",不自动扩到 10。
  • 如果你没指定容量,ArrayList 默认认为你可能需要一个 "常用大小",第一次添加元素时直接扩到 10,减少后续多次扩容的麻烦。

3. MAX_ARRAY_SIZE:数组的 "安全最大容量"

  • Integer.MAX_VALUE - 8(即 2^31-1 - 8,约 21 亿)。
  • 作用:限制 ArrayList 的最大容量,避免创建数组时因内存不足导致崩溃。
  • 为什么减 8?
    因为数组在内存中除了存储元素,还需要存储一些 "头信息"(比如长度、类型等),这部分信息大约占 8 字节。预留这部分空间,能避免数组容量太大时,连头信息都存不下而抛出OutOfMemoryError
  • 特殊情况:如果确实需要超大容量(超过MAX_ARRAY_SIZE),会直接用Integer.MAX_VALUE(但此时有内存溢出风险)。

构造函数

ArrayList 提供了多个构造函数,常用的有以下几种:

无参构造函数

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

使用无参构造函数创建 ArrayList 时,elementData 会被初始化为一个空数组 DEFAULTCAPACITY_EMPTY_ELEMENTDATA,此时数组长度为 0。在首次添加元素时,会进行扩容操作,默认容量为 10。

指定初始容量的构造函数


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

当传入的初始容量大于 0 时,会创建一个指定长度的数组;若初始容量为 0,则使用空数组 EMPTY_ELEMENTDATA;若传入负数,则抛出 IllegalArgumentException 异常。

根据集合创建 ArrayList

public ArrayList(Collection<? extends E> c) {
    elementData = c.toArray();
    if ((size = elementData.length) != 0) {
        // c.toArray might (incorrectly) not return Object[] (see 6260652)
        if (elementData.getClass() != Object[].class)
            elementData = Arrays.copyOf(elementData, size, Object[].class);
    } else {
        // replace with empty array.
        this.elementData = EMPTY_ELEMENTDATA;
    }
}

该构造函数会将传入集合中的元素复制到 elementData 数组中。

扩容机制

当向 ArrayList 中添加元素时,如果当前数组的容量不足以容纳新元素,就需要进行扩容操作。以下是 add 方法和扩容相关的源码:

public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // 确保数组容量足够
    elementData[size++] = e;
    return true;
}
private void ensureCapacityInternal(int minCapacity) {
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

private static int calculateCapacity(Object[] elementData, int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return 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);
}

扩容的核心步骤如下:

  1. 计算需要的最小容量 minCapacity
  2. 判断当前数组容量是否足够,如果不够则调用 grow 方法进行扩容。
  3. 在 grow 方法中,新容量为旧容量的 1.5 倍(oldCapacity + (oldCapacity >> 1))。
  4. 如果新容量小于 minCapacity,则将新容量设置为 minCapacity
  5. 如果新容量超过了数组的最大容量 MAX_ARRAY_SIZE,则调用 hugeCapacity 方法进行大容量扩容。
  6. 最后使用 Arrays.copyOf 方法将旧数组元素复制到新数组。

元素的添加和删除

添加元素

除了前面提到的 add(E e) 方法,ArrayList 还提供了 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++;
}

该方法需要先进行范围检查,然后确保数组容量足够,接着使用 System.arraycopy 方法将 index 及其后面的元素向后移动一位,最后将新元素插入到指定位置。

删除元素

ArrayList 提供了 remove(int index) 方法用于删除指定位置的元素:

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; // 帮助 GC

    return oldValue;
}

该方法先进行范围检查,然后获取要删除的元素,接着使用 System.arraycopy 方法将 index 后面的元素向前移动一位,最后将最后一个元素置为 null,以便垃圾回收器回收。

元素的访问

ArrayList 提供了 get(int index) 方法用于访问指定位置的元素:


public E get(int index) {
    rangeCheck(index);

    return elementData(index);
}

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

该方法先进行范围检查,然后直接从数组中获取指定位置的元素,时间复杂度为 O(1)。

总结

ArrayList底层采用的是数组来存储元素,根据数组的特性,ArrayList在随机访问时效率极高,在增加和删除元素时效率偏低,因为在增加和删除元素时会涉及到数组中元素位置的移动。ArrayList在扩容时每次扩容到原来的1.5倍