ArrayList 源码分析

本文首发于公众号:JavaArchJourney

ArrayList介绍

ArrayList特点如下

  • 动态数组 :使用动态数组实现,能够根据需要自动动态扩容。
  • 随机访问 :由于 ArrayList 基于数组实现,所以支持通过索引快速访问集合中的元素,这使得它在需要频繁按位置访问元素的情况下非常高效。
  • 线程不安全 :默认情况下,ArrayList 不是同步(线程安全)的。在多线程场景下,如果需要线程安全的列表,可以选择使用 Collections.synchronizedList(List l) 方法将 ArrayList 包装成一个线程安全的列表,或者直接使用并发包 java.util.concurrent 中的 CopyOnWriteArrayList 类。
  • 序列化ArrayList 实现了 Serializable 接口,支持将对象状态保存到字节流中以便于存储或传输。为了优化性能,ArrayList 使用 transient 关键字修饰内部数组elementData 并通过自定义序列化逻辑来仅序列化实际存储的元素而非整个数组。
  • 迭代器支持ArrayList 提供了对 IteratorListIterator 的支持,允许遍历集合中的元素,同时提供了移除元素的能力(通过 ListIterator)。

ArrayList的类继承结构如下

  • 继承自AbstractListAbstractList 提供了对 List 接口的部分实现,使得 ArrayList 不需要重新实现这些基础方法。
  • 实现了 List<E> 接口List 接口定义了允许重复元素的有序集合(即列表)。ArrayList 实现了这个接口,提供了所有声明的方法,如添加、删除、获取元素等操作。
  • 实现了 RandomAccess 接口 :这是一个标记接口,没有包含任何方法。实现 RandomAccess 接口表明 ArrayList 支持快速随机访问,这意味着通过索引访问元素的操作在 ArrayList 中是非常高效的。虽然所有List实现都支持随机访问,但由于底层结构的差异,它们的访问速度可能不同,因此并非所有List集合都能提供这种"快速"的随机访问功能。
  • 实现了 Cloneable 接口 :表示支持对象的克隆操作。ArrayList 提供了 clone() 方法来创建当前列表的一个浅拷贝。
  • 实现了 Serializable 接口 :表示 ArrayList 支持序列化操作,可用于对象的持久化存储或网络传输。

ArrayList源码分析

jdk1.8 版本为例,ArrayList 源码的核心方法剖析如下:

存储结构

java 复制代码
public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
    /**
     * 默认初始化容量
     */
    private static final int DEFAULT_CAPACITY = 10;

    /**
     * 最大数组容量
     * 为什么要减去8:为了预留空间以应对JVM内部开销和潜在的实现限制(比如每个对象都有一定量的对象头开销、数组可能需要额外的空间来存储数组的长度信息、某些 JVM 可能需要额外的空间用于内部管理等),通过预留一些空间,可以增加代码的稳定性和兼容性,避免由于接近系统或JVM内部限制而导致的问题。这有助于防止潜在的错误或不稳定行为,特别是在不同的运行环境和平台上。
     */
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

    /**
     * 用于空实例的共享空数组对象。
     * 当用户指定容量为0时,返回的是该空数组。
     */
    private static final Object[] EMPTY_ELEMENTDATA = {};

    /**
     * 用于默认大小空实例的共享空数组对象。
     * 当调用无参构造方法,返回的是该空数组。
     */
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

    /**
     * 保存 ArrayList 数据元素的数组。ArrayList的容量就是该数组的长度。
     * 当 elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA 时,在第一次添加元素进入ArrayList时,数组大小将扩容到DEFAULT_CAPACITY值:10。
     * 标记为 transient :在对象被序列化的时候,elementData 数组不会被序列化。ArrayList 类重写了 writeObject 和 readObject 方法,从而实现了自定义的序列化逻辑,只对实际存储的元素进行序列化处理,而不是整个 elementData 数组。这样不仅减少了序列化后的数据量,还提高了序列化和反序列化的性能。
     */
    transient Object[] elementData; // non-private to simplify nested class access

    /**
     * 数组长度:ArrayList 所包含的元素个数
     */
    private int size;

}

构造函数

无参构造

java 复制代码
/**
 * 无参构造函数:构造一个初始容量为10的空列表。
 * 由前面的属性可知,DEFAULTCAPACITY_EMPTY_ELEMENTDATA 是一个空的 Object[] ,也就是说初始化时其实是一个空数组,当使用 add 方法添加第一个元素时,elementData 数组容量才变成 10。
 */
public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

指定容量构造

java 复制代码
/**
 *
 * 构造一个指定容量为 initialCapacity 的空列表。
 */
public ArrayList(int initialCapacity) {
    if (initialCapacity > 0) {
        // 如果初始容量大于0,则创建指定初始容量大小的数组
        this.elementData = new Object[initialCapacity];
    } else if (initialCapacity == 0) {
        // 如果初始容量等于0,将空数组赋给elementData
        this.elementData = EMPTY_ELEMENTDATA;
    } else {
        // 如果初始容量小于0,抛出异常
        throw new IllegalArgumentException("Illegal Capacity: "+
                initialCapacity);
    }
}

指定集合构造

java 复制代码
/**
 * 构造一个包含指定 collection 的元素的列表,这些元素是按照该 collection 的迭代器返回它们的顺序排列的。
 */
public ArrayList(Collection<? extends E> c) {
    // 将指定 collection 转换为数组,直接赋给 elementData
    elementData = c.toArray();

    if ((size = elementData.length) != 0) {
        // 如果数组的实际大小值大于 0 、且 elementData 不是 Object 类型数据,则执行 Arrays.copy 方法,把 collection 对象的内容深拷贝到 elementData 中
        if (elementData.getClass() != Object[].class)
            elementData = Arrays.copyOf(elementData, size, Object[].class);
    } else {
        // 如果数组的实际大小等于0,将空数组 EMPTY_ELEMENTDATA 赋给 elementData
        this.elementData = EMPTY_ELEMENTDATA;
    }
}

操作方法

add(E e)

java 复制代码
/**
 * 将指定的元素追加到此列表的末尾
 */
public boolean add(E e) {
    // 检查数组容量,如果不够,则容量加1,保证加入的对象有空间可以存储
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    // 把元素赋值到数组末尾
    elementData[size++] = e;
    return true;
}

/**
 * 检查数组容量,在容量不够时进行自动扩容
 * private表明只供类内部使用
 */
private void ensureCapacityInternal(int minCapacity) {
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

/**
 * 根据当前数组元素和给定的最小容量,计算数组所需容量
 */
private static int calculateCapacity(Object[] elementData, int minCapacity) {
    // 如果当前数组元素为空数组(初始情况),返回默认容量(DEFAULT_CAPACITY == 10)和最小容量中的较大值作为所需容量
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    // 否则直接返回最小容量
    return minCapacity;
}

/**
 * 保证数组容量至少为minCapacity,若容量不足则自动扩容
 */
private void ensureExplicitCapacity(int minCapacity) {
    // 将修改次数(modCount)自增1
    modCount++;

    // 若当前数组容量小于minCapacity,则扩充数组容量
    if (minCapacity - elementData.length > 0)
        // 调用grow方法执行扩容
        grow(minCapacity);
}

/**
 * 执行数组扩容
 */
private void grow(int minCapacity) {
    // oldCapacity为旧容量,即当前数据的实际容量
    int oldCapacity = elementData.length;
    // 新容量newCapacity计算:
    // 使用位运算代替乘除运算,性能更优:oldCapacity右移一位,等同于oldCapacity/2
    // 因此,新容量计算结果为旧容量的1.5倍
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    // 检查新容量是否大于最小需求容量,若否,则就把新容量设置为最小需求容量
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    // 若计算的新容量值超出了最大容量临界值,则进行大容量分配
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    // minCapacity is usually close to size, so this is a win:
    // 根据计算得到的新容量,执行扩容:
    // Arrays.copyOf方法会根据指定的新容量newCapacity创建一个新的数组,并将原数组elementData中的元素复制到新数组中
    elementData = Arrays.copyOf(elementData, newCapacity);
}

/**
 * 比较 minCapacity 和 MAX_ARRAY_SIZE,获取最大容量
 */
private static int hugeCapacity(int minCapacity) {
    // 如果 minCapacity < 0,抛出异常
    if (minCapacity < 0)
        throw new OutOfMemoryError();
    // 如果想要的容量(最小需求容量)大于MAX_ARRAY_SIZE,则分配Integer.MAX_VALUE(2147483647),否则分配MAX_ARRAY_SIZE(2147483639)。
    return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
}

ArrayList 中添加元素时,如果当前的容量不足以容纳新添加的元素,就会触发动态扩容机制:

  • 检查容量 :当调用 add 方法试图向 ArrayList 中添加一个新元素时,首先会检查当前数组 elementData 的容量是否足够。如果当前容量足以容纳新元素,则直接将元素添加到列表的末尾。
  • 计算新容量 :如果当前容量不足以容纳新元素,则需要进行扩容。默认情况下,新的容量是当前容量的 1.5 倍
    • 这个计算方式可能会根据不同的JDK版本有所变化,但目的是逐步增加容量以减少频繁扩容带来的性能开销。
  • 创建新数组并复制数据 :确定了新的容量后,ArrayList 会使用 Arrays.copyOf 方法,创建一个新的、更大容量的数组,将原数组中的所有元素复制到新数组中。
  • 更新引用 :最后,ArrayList 将内部的 elementData 引用指向这个新创建的数组,并将新元素添加到适当的位置。

这种机制保证了 ArrayList 可以随着元素的添加自动增长其容量,而不需要用户手动调整大小。但由于涉及到数组复制操作,每次扩容都会带来一定的性能开销,所以为了优化性能,应尽量预估好初始容量,避免频繁的扩容操作。

add(int index, E element)

java 复制代码
/**
 * 在列表的指定位置插入指定元素
 */
public void add(int index, E element) {
    // 对index进行界限检查:若index的位置超出数组的大小或者小于0,则抛异常IndexOutOfBoundsException
    rangeCheckForAdd(index);
    // 检查数组容量,如果不够,则容量加1,保证加入的对象有空间可以存储。与 add(E element) 流程类似
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    // 使用 System.arraycopy 把需要插入的位置(index)后面的元素都往后移动一位
    System.arraycopy(elementData, index, elementData, index + 1,
            size - index);
    // 将element插入index位置
    elementData[index] = element;
    size++;
}

private void rangeCheckForAdd(int index) {
    if (index > size || index < 0)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

addAll(int index, Collection c)

java 复制代码
/**
 * 将指定集合中的所有元素插入到列表指定位置
 */
public boolean addAll(int index, Collection<? extends E> c) {
    // 检查index边界:若index大于数组实际大小、或小于0,则抛出IndexOutOfBoundsException异常
    rangeCheckForAdd(index);

    Object[] a = c.toArray();
    int numNew = a.length;
    // 数组扩容:检查数组容量,如果不够,则容量加numNew,保证加入的对象有空间可以存储
    ensureCapacityInternal(size + numNew);  // Increments modCount

    // 需要移动的元素个数
    int numMoved = size - index;
    if (numMoved > 0)
        // 数组从index位置开始的所有元素,往后移numNew个位置,腾出空间
        System.arraycopy(elementData, index, elementData, index + numNew,
                numMoved);

    // 在index处插入collection
    System.arraycopy(a, 0, elementData, index, numNew);
    size += numNew;
    return numNew != 0;
}

private void rangeCheckForAdd(int index) {
    if (index > size || index < 0)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

remove(int index)

java 复制代码
/**
 * 删除列表中指定位置上的元素
 */
public E remove(int index) {
    // 检查index是否在范围内,否则报错
    rangeCheck(index);
    // 修改次数加1
    modCount++;
    // 将要删除的元素赋值给oldValue
    E oldValue = elementData(index);

    // 删除其实就是将任何后续元素移动到左侧(从其索引中减去一个元素)。
    // numMoved是需要往左移的元素个数
    int numMoved = size - index - 1;
    if (numMoved > 0)
        // 把index后的所有元素(numMoved个)都左移一位,通过拷贝覆盖实现删除
        System.arraycopy(elementData, index + 1, elementData, index,
                numMoved);
    // 清理对象引用,给GC回收
    elementData[--size] = null; // clear to let GC do its work

    // 返回从列表中删除的元素
    return oldValue;
}

private void rangeCheck(int index) {
    if (index >= size)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

@SuppressWarnings("unchecked")
E elementData(int index) {
    return (E) elementData[index];
}

remove(Object o)

java 复制代码
/**
 * 从列表中删除指定元素(如果存在)。
 * 如果列表不包含该元素,则无更改。
 * 如果列表中包含多个该元素,则只删除第一个出现的。
 * 如果此列表包含指定的元素,返回true。
 */
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 {
        // 删除与指定元素相同的元素(通过equals比较是否相同)
        for (int index = 0; index < size; index++)
            if (o.equals(elementData[index])) {
                fastRemove(index);
                return true;
            }
    }
    return false;
}

/*
 * 快速移除:私有方法,跳过了边界检查,并且不返回被移除的值。
 */
private void fastRemove(int index) {
    modCount++;
    // numMoved是需要往左移的元素个数
    int numMoved = size - index - 1;
    if (numMoved > 0)
        // 把index后的所有元素(numMoved个)都左移一位,通过拷贝覆盖实现删除
        System.arraycopy(elementData, index + 1, elementData, index,
                numMoved);
    // 清理对象引用,给GC回收
    elementData[--size] = null; // clear to let GC do its work
}

clear()

java 复制代码
/**
 * 从列表中删除所有元素
 */
public void clear() {
    modCount++;

    // clear to let GC do its work
    // 把数组中所有的元素的值设为null,清理对象引用后才能被GC回收
    for (int i = 0; i < size; i++)
        elementData[i] = null;

    size = 0;
}

trimToSize()

java 复制代码
/**
 * 把数组容量缩小到当前列表的实际大小(size)。 
 * 可以使用此操作来最小化ArrayList实例的存储。
 */
public void trimToSize() {
    modCount++;
    if (size < elementData.length) {
        elementData = (size == 0)
                // 若size == 0,则使用空数组实例
                ? EMPTY_ELEMENTDATA
                // 若size > 0,则创建一个size大小的数组,把元素拷贝过去
                : Arrays.copyOf(elementData, size);
    }
}

get(int index)

java 复制代码
/**
 * 返回此列表中指定位置的元素。
 */
public E get(int index) {
    // 检查index下标
    rangeCheck(index);
    // 返回当前下标位置的元素
    return elementData(index);
}

private void rangeCheck(int index) {
    if (index >= size)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

@SuppressWarnings("unchecked")
E elementData(int index) {
    return (E) elementData[index];
}

set(int index, E element)

java 复制代码
/**
 * 把列表中指定位置的元素替换为指定的元素                                  。
 */
public E set(int index, E element) {
    // 对index进行界限检查
    rangeCheck(index);

    E oldValue = elementData(index);
    elementData[index] = element;
    // 返回原来在这个位置上的元素
    return oldValue;
}

private void rangeCheck(int index) {
    if (index >= size)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

@SuppressWarnings("unchecked")
E elementData(int index) {
    return (E) elementData[index];
}

核心原理总结

  • ArrayList 通过一个可动态调整大小的数组来实现列表操作,允许快速随机访问元素,但插入和删除操作可能需要移动元素以维护数组的连续性。
  • 重要成员变量:
    • transient Object[] elementData;:使用数组 存储列表元素。
      • 使用 transient 关键字标记以避免默认序列化整个数组,因为实际使用的可能只是数组的一部分。
      • 若不指定数组初始容量,则默认初始容量是 10
    • int size;:表示列表中实际存储的元素数量。
  • ArrayList 中允许元素为null
  • 增(Add):向列表添加元素。如果当前容量不足以容纳新元素,则会调用 grow() 方法进行动态扩容
  • 动态扩容机制:当添加元素导致容量不足时,ArrayList 会自动扩容。新的容量计算方式通常是原容量的 1.5 倍 左右。扩容时,会使用 Arrays.copyOf() 创建一个新的更大容量的数组,并将所有原有元素复制过去。
    • ensureCapacity(int minCapacity):当预先知道需要增加很多元素时,可以手动调用此方法来增加 ArrayList 的容量,以减少扩容次数。
    • trimToSize():将 ArrayList 实例的容量调整为当前列表大小,减少空间浪费。
  • 删(Remove):移除指定索引处的元素,实际上就是把该索引之后的所有元素向前移动 x 位来填补空缺,同时减少列表的 size
  • 查/改(Get/Set):直接返回/更新数组中指定索引处的元素,数组结构使得查改操作非常高效。
相关推荐
洛阳泰山21 分钟前
基于 Easy Rules 的电商订单智能决策系统:构建可扩展的业务规则引擎实践
java·开发语言·规则引擎·easy rules
THXW.28 分钟前
【Java项目与数据库、Maven的关系详解】
java·数据库·maven
架构师沉默32 分钟前
外卖平台每天1000万订单查询,是如何扛住高并发的?
java·后端·架构
kushu71 小时前
Java 包
java·开发语言
bug菌1 小时前
🤔领导突然考我Spring中的注解@Bean,它是做什么用的?我...
java·后端·spring
寒士obj2 小时前
熟悉并使用Spring框架 - 注解篇
java·spring
BricheersZ2 小时前
LangChain4J-(1)-Hello World
java·人工智能·langchain
回家路上绕了弯2 小时前
Spring ApplicationContext 源码深度剖析:容器的核心引擎
java·spring
心月狐的流火号2 小时前
线程池ThreadPoolExecutor源码分析(JDK 17)
java·源码阅读