JavaCore:ArrayList源码解析与性能优化

前言

由于长期做业务开发忽略了对 Java 底层的了解和掌握,所以为了巩固基础将会从源码层面,借助 IntelliJ IDEA、通义灵码、Sequence Diagram、Grok 等工具以源码的形式学习和巩固相关基础实现并掌握其设计思路。

所用的 JDK 版本是 1.8.0_462,环境是Zulu 8.88.0.19-CA-macos-aarch64

参考文档主要用的是 Oracle 官方的 8 版本官方文档(English),主要还是以源码为主。如果有参考其他相关文章会在参考中列出。

因作者水平有限,如有理解错误还请指正,谢谢~。

什么是 ArrayList

ArrayList是一个动态数组(可以理解成一个能自动扩容的数组)。它位于 java.util包里,底层是用数组实现的,但是普通数组灵活得多,因为它可以随机增加或减少元素数量。

它的特点:

  • 动态大小:不像普通数组大小固定,ArrayList 可以根据需要自动扩容或缩容。
  • 有序:元素按照添加顺序存储,可以根据索引(索引访问从 0 开始)快速访问。
  • 允许重复:可以存多个相同的元素。
  • 线程不安全:多线程环境下直接用 ArrayList 会出问题。
  • 存对象:ArrayList 只能存对象(比如 Integer、String 等),不能直接存基本数据类型(int、double),但是可以用包装类搞定。

继承/实现关系的说明

继承:让子类继承父类的属性和方法,并可以扩展或重写功能。

实现:接口定义了一组方法规范(行为),类必须实现这些方法,从而保证自己具备接口约定的功能。

  • Iterable它是所有可迭代对象的顶层接口,让对象可以通过for-each循环或Iterator遍历元素。
  • Collection<E>是Java集合框架的顶层接口。它定义了一组操作元素的通用方法,适用于任何集合类型(List、Set、Queue等)。
  • List<E>定义了一个有序、可索引的集合行为,ArrayList通过实现它,成为一个标准的列表(List)类型。
  • AbstractCollection<E>是一个抽象类,提供了集合框架的基础功能。
  • AbstractList<E>是一个抽象类,是 AbstractCollection<E>的子类,提供了列表的通用功能。
  • Cloneable表示 ArrayList 支持克隆(复制对象)。
  • RandomAccess这是一个标记接口(没方法),表示ArrayList支持高效的随机访问。
  • Serializablejava.io包里的一个接口,没有定义任何方法。它的作用是告诉Java虚拟机(JVM):这个类的对象可以被序列化(转成字节流)和反序列化(从字节流恢复成对象)。

基本使用

csharp 复制代码
public static void main(String[] args) {
    // 创建一个ArrayList
    ArrayList<String> list = new ArrayList<>();

    // 添加元素
    list.add("苹果");
    list.add("香蕉");
    list.add("橙子");

    // 添加已有集合到当前集合末尾
    list.addAll(list);

    // 删除元素
    list.remove("香蕉");

    // 获取列表大小
    int size = list.size();

    // 获取索引位置为0的元素
    list.get(0);

    // 判断列表中是否包含苹果,返回true
    list.contains("苹果");

    // 判断列表是否为空,返回false
    list.isEmpty();

    // list.indexOf("橙子") 获取苹果在列表中的第一个索引位置,返回:1
    list.indexOf("橙子");

    // 获取橙子在列表中的最后一个索引位置,返回:4
    list.lastIndexOf("橙子");

    // 获取索引位置为3到列表末尾的子列表,返回:[香蕉, 橙子]
    list.subList(3, list.size());

    // 将苹果换成草莓
    list.set(0, "草莓"); // 将索引为0的元素替换为草莓
    // list.set(list.indexOf("苹果"), "草莓");

    // 打印列表
    System.out.println("当前元素:" + list + ",当前元素大小:" + size); 
    // 输出: 当前元素:[草莓, 橙子, 苹果, 香蕉, 橙子],当前元素大小:5
}

源码分析

创建对象

在 ArrayList 中创建对象主要有三种方式:无参数的构造方法、通过指定初始容量的构造方法、用现有集合初始化。

无参数的构造方法

ArrayList()创建一个默认空列表。适用场景:不知道初始大小,动态添加元素。

arduino 复制代码
/**
 * 默认空容量数组,长度为0
 */
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

/**
 * Constructs an empty list with an initial capacity of ten.
 */
public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

在创建时,会使用 DEFAULTCAPACITY_EMPTY_ELEMENTDATA来创建一个空的对象数组。

指定初始容量

ArrayList(int initialCapacity)创建一个指定容量的空列表。

适用场景:知道大概的一个数据容量,这样数据数据量较大时可以避免的扩容。

⚠️注意:如果初始化长度小于零时会抛出 IllegalArgumentException 的异常。每次扩容是 1.5 倍增长,太小容易频繁扩容,太大空间浪费内存。

java 复制代码
/*
用于表示空实例的共享空数组实例。通过共享同一个空数组实例来优化内存使用。主要功能:
    共享空数组:为所有需要空数组的实例提供一个统一的空数组引用
    节省内存:避免每次创建空数组时都分配新的内存空间
    初始化用途:通常用作集合类等数据结构的默认初始值
*/
private static final Object[] EMPTY_ELEMENTDATA = {};

// 集合真正存储数据的容器
transient Object[] elementData; // non-private to simplify nested class access

// 在创建时传入一个参数 initialCapacity用于指定长度;
public ArrayList(int initialCapacity) {
    // 如果长度大于 0 使用来这个参数来创建一个对象数组;
    // 如果为 0 创建一个长度为 0 的空对象数组;
    // 如果小于 0 则抛出IllegalArgumentException 异常信息。
    if (initialCapacity > 0) {
        this.elementData = new Object[initialCapacity];
    } else if (initialCapacity == 0) {
        this.elementData = EMPTY_ELEMENTDATA;
    } else {
        throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity);
    }
}

从集合创建

ArrayList(Collection c)用现有集合初始化。适用场景:需要复制或者转换集合,比如从 Set 集合转 List 集合。

⚠️注意:新ArrayList包含传入集合的所有元素,顺序取决于原集合的迭代顺序。传入集合必须是Collection的子类型,且元素类型兼容(? extends E)。

ini 复制代码
// 记录了ArrayList中实际包含的元素个数,用于跟踪当前存储的元素数量。
private int size;

/*
用于表示空实例的共享空数组实例。通过共享同一个空数组实例来优化内存使用。主要功能:
    共享空数组:为所有需要空数组的实例提供一个统一的空数组引用
    节省内存:避免每次创建空数组时都分配新的内存空间
    初始化用途:通常用作集合类等数据结构的默认初始值
*/
private static final Object[] EMPTY_ELEMENTDATA = {};

public ArrayList(Collection<? extends E> c) {
    Object[] a = c.toArray();
    if ((size = a.length) != 0) {
        if (c.getClass() == ArrayList.class) {
            elementData = a;
        } else {
            elementData = Arrays.copyOf(a, size, Object[].class);
        }
    } else {
        // replace with empty array.
        elementData = EMPTY_ELEMENTDATA;
    }
}
  1. 将指定集合 c 中的元素转换为数组a
  2. 如果数组长度不为 0:

2.1 若 c 是 ArrayList 类型,直接使用该数组,否则复制数组到新的Object数组;

2.2 如果数组长度为 0,使用空数组常量;

使用Arrays.asList()

适用场景:快速用数组初始化 ArrayList 。

csharp 复制代码
ArrayList<String> list = new ArrayList<>(Arrays.asList("苹果", "香蕉", "橙子"));
list.add("草莓"); // 可以修改
System.out.println(list); // 输出: [苹果, 香蕉, 橙子, 草莓]

注意:

  1. Arrays.asList()返回一个固定大小的List,不能直接add/remove。
  2. new ArrayList<>(Arrays.asList(...))创建可修改的 ArrayList。

添加元素

往列表末尾加元素

整体实现流程:

  1. 首先通过 ensureCapacityInternal确保容量足够,若不足则通过 grow()方法进行扩容
  2. 然后将元素插入到 elementData 数组的末尾,并更新 size。
  3. 方法始终返回true。
arduino 复制代码
public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!

    // 将元素插入到 elementData 数组的末尾,并更新size
    elementData[size++] = e;
    return true;
}

在指定索引位置插入元素

在指定索引位置插入元素,后面的元素会自动后移

整体实现流程:

首先检查索引是否合法,然后确保容量足够,接着将插入位置及之后的元素右移一位,最后在指定位置插入新元素并增加大小。

scss 复制代码
public void add(int index, E element) {
    // 检查索引是否合法
    rangeCheckForAdd(index);

    // 检查容量是否充足,不足进行扩容
    ensureCapacityInternal(size + 1);  // Increments modCount!!

    // 用于将一个数组中的元素复制到另一个数组中,这里是将旧数据复制到新数组中
    System.arraycopy(elementData, index, elementData, index + 1, size - index);

    // 在指定位置插入新元素
    elementData[index] = element;

    // 增加实际元素大小
    size++;
}

扩容参考:扩容实现

arduino 复制代码
// 检查索引是否越界
private void rangeCheckForAdd(int index) {
    // 指定的索引大于当前的数据长度或者索引小于0,就抛出IndexOutOfBoundsException异常
    if (index > size || index < 0)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

// 索引越界的具体信息
private String outOfBoundsMsg(int index) {
    return "Index: "+index+", Size: "+size;
}

往末尾添加集合

指定集合中的所有元素追加到当前ArrayList的末尾。

整体流程:

首先将集合并入数组,然后确保容量足够,接着通过 System.arraycopy复制元素,并更新 size。若添加了新元素则返回true,否则 false。

ini 复制代码
public boolean addAll(Collection<? extends E> c) {
    Object[] a = c.toArray();
    int numNew = a.length;
    ensureCapacityInternal(size + numNew);  // Increments modCount
    System.arraycopy(a, 0, elementData, size, numNew);
    size += numNew;
    return numNew != 0;
}

扩容参考:扩容实现

修改元素

整体流程:首先检查索引是否越界,然后获取原位置的旧值,将新元素赋值到该位置,并返回旧值。

scss 复制代码
public E set(int index, E element) {
    rangeCheck(index);

    E oldValue = elementData(index);
    elementData[index] = element;
    return oldValue;
}

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

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

删除元素

根据索引位置移除

整体流程:

首先检查索引是否越界,然后获取旧值,接着通过 System.arraycopy 将后续元素前移一位,最后将末尾元素置为 null以帮助垃圾回收,并返回被删除的元素。

scss 复制代码
public E remove(int index) {
    rangeCheck(index);

    // 记录修改次数
    modCount++;
    E oldValue = elementData(index);
    
    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index, numMoved);
    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));
}

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

移除指定元素

用于删除列表中首次出现的指定元素。

整体流程:

  1. 若参数为 null ,则遍历数组查找第一个 null 元素并删除;
  2. 若参数不为null,则通过equals方法查找匹配元素并删除;

删除成功后返回 true ,否则返回 false。

删除操作由 fastRemove 方法完成,不返回被删元素,直接移动后续元素并置空末尾以助垃圾回收。

arduino 复制代码
public boolean remove(Object o) {
    if (o == 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;
}

/*
首先增加修改计数器modCount,然后计算需要移动的元素个数 numMoved。
如果需要移动元素,则使用System.arraycopy将后面的元素向前复制。
最后将原末尾元素置为null,帮助垃圾回收。
*/
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; // clear to let GC do its work
}

获取元素

csharp 复制代码
// 创建一个空的ArrayList,默认容量10
ArrayList<String> arrayList = new ArrayList<>();

// 添加元素
arrayList.add("Java");
arrayList.add("Python");
arrayList.add("Go");

// for 循环,性能高,适用于需要通过索引访问和修改元素的需求,需要手工管理索引、注意索引越界的相关问题。
for (int i = 0; i < arrayList.size(); i++) {
    System.out.println("item data:" + arrayList.get(i));
}

// for 循环,适用于只读区元素的需求。
for (String item : arrayList) {
    System.out.println("item data:" + item);
}

// forEach,依赖于 Iterator,适用于所有实现 Iterable 的集合。适用于读取元素的需求。代码简洁,可读性强。
arrayList.forEach(item -> System.out.println("元素:" + item));

// Iterator 迭代器,适用于所有实现Iterable的集合。适用于在读取元素时控制逻辑(比如边读取边修改和删除元素)。
Iterator<String> iterator = arrayList.iterator();
while (iterator.hasNext()){
    // 获取下一个元素
    String next = iterator.next();
    System.out.println("item data:" + next);
}

案例:使用以上四种方式计算总和

ini 复制代码
ArrayList<Integer> calcSum = new ArrayList<>();
calcSum.add(10);
calcSum.add(20);
calcSum.add(30);
calcSum.add(40);

// 1. 普通for循环
int sum1 = 0;
for (int i = 0; i < calcSum.size(); i++) {
    sum1 += calcSum.get(i);
}
System.out.println("普通for循环总和:" + sum1);

// 2. 增强for循环
int sum2 = 0;
for (Integer item : calcSum) {
    sum2 += item;
}
System.out.println("增强for循环总和:" + sum2);

// 3. Iterator迭代器
int sum3 = 0;
Iterator<Integer> iterator = calcSum.iterator();
while (iterator.hasNext()) {
    sum3 += iterator.next();
}
System.out.println("Iterator迭代器总和:" + sum3);

// 4. Lambda和Stream
int sum4 = calcSum.stream().mapToInt(Integer::intValue).sum();
System.out.println("Lambda和Stream总和:" + sum4);

扩容机制

观察扩容

ini 复制代码
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
    ArrayList<String> list = new ArrayList<>();

    // 存储扩容次数
    Set<Integer> set = new HashSet<>(50);

    int size = 500;
    for (int i = 0; i < size; i++) {
        list.add(String.valueOf(i));

        // 反射获取elementData数组长度,打印容量
        Field elementDataFieid = ArrayList.class.getDeclaredField("elementData");
        elementDataFieid.setAccessible(true);
        Object[] elementData = (Object[]) elementDataFieid.get(list);
        System.out.println("添加第 " + i + " 个元素,当前容量:" + elementData.length);
        set.add(elementData.length);
    }
    System.out.println("扩容次数:" + set.size());
}

/*
输出:
添加第 0 个元素,当前容量:10
添加第 10 个元素,当前容量:15
添加第 15 个元素,当前容量:22
添加第 499 个元素,当前容量:549
添加第 22 个元素,当前容量:33
添加第 33 个元素,当前容量:49
添加第 49 个元素,当前容量:73
添加第 73 个元素,当前容量:109
添加第 109 个元素,当前容量:163
添加第 163 个元素,当前容量:244
添加第 244 个元素,当前容量:366
添加第 366 个元素,当前容量:549
扩容次数:11
 */

扩容实现

说明:由于 ArrayList 源码中多处涉及到了扩容(添加单个【索引、元素】、添加集合【索引、元素】),为了简短文章篇幅,所以将其抽出来进行解释。

arduino 复制代码
// 调用 ensureCapacityInternal 检查容量是否充足;
private void ensureCapacityInternal(int minCapacity) {
    // 调用 calculateCapacity 计算实际所需的容量,如果当前数组是默认空数组,则返回默认容量 10 与所需最小容量中的较大的值;否则直接返回所需最小容量。
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
arduino 复制代码
// 默认空容量数组,长度为0
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

// 默认初始大小
private static final int DEFAULT_CAPACITY = 10;

// 计算最小容量
private static int calculateCapacity(Object[] elementData, int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;
}
scss 复制代码
// 检查容量是否充足
private void ensureExplicitCapacity(int minCapacity) {
    // 用于检测并发修改。
    modCount++;

    // 如果当前的数据长度(elementData)小于所需容量(minCapacity),则调用 grow() 方法进行数组扩容
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}
arduino 复制代码
// 数组最大长度:2^31(2147483647) - 8
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

private void grow(int minCapacity) {
    // 获取元素的实际容量长度作为旧容量 oldCapacity
    int oldCapacity = elementData.length;
    
    // 计算新容量长度 newCapacity 为原来容量的1.5倍,旧容量长度 + 旧容量长度的一半(通过移位实现)
    int newCapacity = oldCapacity + (oldCapacity >> 1);

    // 如果新的容量小于所需最小容量,则使用最小容量
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;

    // 若新容量超过最大数组大小,则调用hugeCapacity处理;
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    // minCapacity is usually close to size, so this is a win:

    // 通过Arrays.copyOf将原数组复制到新容量的数组中。
    elementData = Arrays.copyOf(elementData, newCapacity);
}

// 处理数组扩容时的长度边界问题
private static int hugeCapacity(int minCapacity) {
    if (minCapacity < 0) // overflow
        throw new OutOfMemoryError();
    // 如果新容量超过了数组的最大长度则使用使用最大长度(2^31 -1:2147483647),否则就使用数组的最大长度 - 8(2147483639)
    return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE;
}

问题:

  1. 为什么是 1.5 倍,不是 2 倍?
  • 这其实是一个历史的遗留问题。在云计算时代之前,都是物理机器部署成本比较高,那么如何去优化性能就成了一个必须要考虑的问题,如果每次都扩容到 2 倍,那么在大数据量(比如几十万)的情况下就需要去使用更多的内存空间可能会导致内存溢出,如果这些空间没有被充分使用就会产生空间浪费的问题。如果小于 1.5 倍,那么就需要频繁的去复制数组进行扩容(I/O 的开销会比较大)。经过了一些实践验证认为 1.5 倍会比较合理。
  • 其次 2 倍的增长下垃圾回收器 GC 的处理成本也会比较高。
  1. 为什么要通过移位计算,而不是除法?
  • 位移操作是操作的二进制位,CPU 的执行效率比除法计算要高在所有硬件上行为一致,而除法可能因CPU实现略有差异。(ArrayList 从 JDK1.2 版本开始,比较早期);
  • 移位操作自动向下取整,与除法运算结果相同;
  1. Integer.MAX_VALUEMAX_ARRAY_SIZE有什么区别?

Integer.MAX_VALUE是理论上 int 能表示的最大值,分配 Integer.MAX_VALUE 大小的数组(约2GB)可能导致内存溢出,因为JVM需要额外的空间存储元数据。

缩容

缩容定义:缩容是指ArrayList减少底层数组(elementData)的容量,以释放多余的内存空间。

ArrayList中通过 trimToSize方法实现 List 的缩容控制,默认情况下不会自动缩容, 哪怕你remove() 很多元素。

如果想实现缩容的处理,可以通过 ArrayList.trimToSize() 来手动缩容到实际的大小。 不会缩容的主要目的在于:

  1. 避免频繁数组复制操作以此来提高性能,因为缩容的实现是创建一个新的实际大小的数组,然后把数组迁移到新数组上;
  2. 保证容量大于等于需要存储的容量,为后续添加元素预留空间。
arduino 复制代码
public void trimToSize() {
    modCount++;
    if (size < elementData.length) {
        elementData = (size == 0)? EMPTY_ELEMENTDATA : Arrays.copyOf(elementData, size);
    }
}

线程安全问题

什么是fail-fast机制?

fail-fast(快速失败)是 Java 集合在遍历时,如果发现有人改了集合的结构(比如添加元素、移除元素)时就会立即抛出 ConcurrentModificationException异常并停止操作。这就相当于你在算账,突然又有人来消费了,这时你就得被迫终止,因为不论你怎么算账目都是错的。fail-fast 就承担了"监控摄像头"的这个角色。

所以 fail-fast 本质上就是一种保护集合的数据一致性的安全机制,防止在遍历过程中因为意外修改导致程序出错或数据损坏。

与 fail-fast 相反的是 fail-safe机制来实现遍历修改集合内容时保证遍历过程不会被意外打断。原理是每次修改(add、remove等)时,复制一份底层数组,修改在新副本上,迭代器使用旧数组,以实现快照隔离。

为啥需要这个东西?

  • 防止数据不一致问题

ArrayList 的底层是一个数组,遍历时依赖索引或者 Iterator。如果在遍历过程中,列表被修改(比如删除元素、数组元素前移),Iterator 可能访问到错误的位置或索引越界,导致数据发生问题。

  • 数据混乱

ArrayList不是线程安全的,多个线程同时操作(一个遍历,一个修改)可能导致数据混乱。如果需要保证线程安全可以使用 CopyOnWriteArrayList

vbnet 复制代码
public static void main(String[] args) {
    List<String> failFast = new ArrayList<>();
    failFast.add("1");
    failFast.add("2");
    failFast.add("3");
    failFast.add("4");

    Iterator<String> iterator = failFast.iterator();
    while (iterator.hasNext()){
        String next = iterator.next();
        if ("2".equals(next)){
            // 此时就会触发 ConcurrentModificationException
            failFast.remove(next);
        }
    }
    System.out.println(failFast);
}

如何解决?

  • 使用 Iterator
vbnet 复制代码
while (iterator.hasNext()){
    String next = iterator.next();
    if ("2".equals(next)){
        // 安全删除,不会触发fail-fast
        iterator.remove();
    }
}
System.out.println(failFast);	// 输出:[1, 3, 4]
  • 使用 CopyOnWriteArrayList
csharp 复制代码
CopyOnWriteArrayList<String> failFast = new CopyOnWriteArrayList<>();

failFast.add("1");
failFast.add("2");
failFast.add("3");
failFast.add("4");

for (String fruit : failFast) {
    if (fruit.equals("2")) {
        failFast.remove(fruit);
    }
}

System.out.println(failFast); // 输出:[1, 3, 4]
  • 收集后再删除
csharp 复制代码
ArrayList<String> failFast = new ArrayList<>();
failFast.add("1");
failFast.add("2");
failFast.add("3");
failFast.add("4");

// 收集需要删除的元素
ArrayList<String> removeItem = new ArrayList<>();
for (String item : failFast) {
    if ("2".equals(item))
        removeItem.add(item);
}

// 移除元素集合
failFast.removeAll(removeItem);

System.out.println(failFast);	// 输出:[1, 3, 4]

实现原理

  • modCount :ArrayList内部的一个int字段,记录集合的结构修改次数(如add、remove、clear等操作会增加modCount)。
  • Iterator :ArrayList的迭代器(Iterator 类)在创建时会记录modCount的值(存为expectedModCount),并在每次迭代时检查是否一致。
  • 异常触发:如果modCount与expectedModCount不一致,说明集合被修改,抛出ConcurrentModificationException。
csharp 复制代码
/*
用于记录列表结构被修改的次数。当列表大小改变或发生其他结构性变化时,该计数器递增。
迭代器使用此字段检测并发修改,若发现不一致则抛出ConcurrentModificationException,实现快速失败机制。
*/
protected transient int modCount = 0;

// expectedModCount记录迭代器创建时列表的修改次数
int expectedModCount = modCount;

// ArrayList 的内部类
private class Itr implements Iterator<E> {

    public void remove() {
        if (lastRet < 0)
            throw new IllegalStateException();

        // 检查数据版本是否被修改
        checkForComodification();
    
        try {
            ArrayList.this.remove(lastRet);
            cursor = lastRet;
            lastRet = -1;
            expectedModCount = modCount;
        } catch (IndexOutOfBoundsException ex) {
            throw new ConcurrentModificationException();
        }
    }

    /*
    这段代码用于实现迭代器的并发修改检测机制:
        expectedModCount记录迭代器创建时列表的修改次数.
        通过与modCount(列表实际修改次数)比较
    如果两者不一致,说明在迭代过程中列表被外部修改,从而检测到并发修改异常,保证迭代过程的数据一致性
    */
    final void checkForComodification() {
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
    }
}

参考

  1. 开放平台
  2. Java Platform Standard Edition 8 Documentation
相关推荐
Eiceblue14 分钟前
Java实现PDF表格转换为CSV
java·python·pdf
自由的疯1 小时前
Java RuoYi整合Magic-Api详解
java·后端·架构
自由的疯1 小时前
Java 实现TXT文件上传并解析的Spring Boot应用
后端·架构
老华带你飞1 小时前
校园二手书交易|基于SprinBoot+vue的校园二手书交易管理系统(源码+数据库+文档)
java·前端·数据库·vue.js·小程序·毕设·校园二手书交易管理系统
hoho不爱喝酒1 小时前
微服务Eureka组件的介绍、安装、使用
java·微服务·eureka·架构
开始学java2 小时前
抽象类和抽象方法
后端
华仔啊2 小时前
接口卡成PPT?这9个优化技巧让系统飞起来,亲测有效!
java
华仔啊2 小时前
别再乱 new ArrayList!8 大 Java 容器选型案例,一篇看懂
java·后端
小码编匠2 小时前
手把手教会设计 WinForm 高DPI兼容程序,告别字体模糊与控件乱飞(.NET 4.6.1/.NET 6.0)
后端·c#·.net