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):直接返回/更新数组中指定索引处的元素,数组结构使得查改操作非常高效。
相关推荐
木易 士心8 分钟前
Spring AI 核心架构解析:构建企业级 AI 应用的 Java 新范式
java·spring
61900833618 分钟前
linux 安装jdk
java·linux·运维
懂得节能嘛.21 分钟前
【动态配置中心】Java+Redis构建动态配置中心
java·开发语言·redis
专注于大数据技术栈22 分钟前
Java中JDK、JRE、JVM概念
java·开发语言·jvm
YuanlongWang25 分钟前
C# 基础——值类型与引用类型的本质区别
java·jvm·c#
Kay_Liang1 小时前
大语言模型如何精准调用函数—— Function Calling 系统笔记
java·大数据·spring boot·笔记·ai·langchain·tools
自由的疯1 小时前
Java 如何学习Docker
java·后端·架构
自由的疯1 小时前
Java Docker本地部署
java·后端·架构
007php0071 小时前
猿辅导Java面试真实经历与深度总结(二)
java·开发语言·python·计算机网络·面试·职场和发展·golang
摇滚侠1 小时前
Spring Boot 3零基础教程,WEB 开发 内容协商机制 笔记34
java·spring boot·笔记·缓存