Java 集合框架:ArrayList 的介绍、使用、原理与源码解析

时间:2025-01-19 11:21:58

大家好,我是栗筝i,这篇文章是我的 “栗筝i 的 Java 技术栈” 专栏的第 013 篇文章,在 “栗筝i 的 Java 技术栈” 这个专栏中我会持续为大家更新 Java 技术相关全套技术栈内容。专栏的主要目标是已经有一定 Java 开发经验,并希望进一步完善自己对整个 Java 技术体系来充实自己的技术栈的同学。与此同时,本专栏的所有文章,也都会准备充足的代码示例和完善的知识点梳理,因此也十分适合零基础的小白和要准备工作面试的同学学习。当然,我也会在必要的时候进行相关技术深度的技术解读,相信即使是拥有多年 Java 开发经验的从业者和大佬们也会有所收获并找到乐趣。

本文将从介绍 ArrayList 开始,详细探讨其使用方法、工作原理以及背后的源码实现,帮助读者深入理解并灵活运用 ArrayList,以提升编程效率和代码质量。

在接下来的部分中,我们将首先概述 ArrayList 的基本特性及其在 Java 集合框架中的地位。随后,通过实际代码示例展示如何创建、操作和管理 ArrayList。接着,我们会揭示 ArrayList 的内部工作机制,包括其底层数据结构、扩容策略和性能优化等方面的内容。最后,我们将深入分析 ArrayList 的源码,探讨其设计思想和实现细节,以便读者能够更全面地掌握这一重要的集合类。


文章目录

      • 1、ArrayList 概述
        • 1.1、ArrayList 介绍
        • 1.2、ArrayList 特点
        • 1.3、ArrayList 使用用例
        • 1.4、ArrayList 实现原理
        • 1.5、ArrayList 优缺点及适用场景
      • 2、ArrayList 源码
        • 2.1、数据结构
        • 2.2、构造方法
        • 2.3、元素访问
        • 2.4、添加操作
        • 2.5、扩容机制
        • 2.6、删除操作
      • 3、ArrayList 知识点补充
        • 3.1、ArrayList 方法概述
        • 3.2、在遍历 ArrayList 时移除元素时的问题
          • 3.2.1、ConcurrentModificationException
          • 3.2.2、跳过元素
        • 3.3、遍历 ArrayList 时移除元素时的问题解决方案
          • 3.3.1、使用迭代器的 remove 方法
          • 3.3.2、使用 Java8 的 removeIf 方法
          • 3.3.3、使用逆向遍历


1、ArrayList 概述

1.1、ArrayList 介绍

ArrayList 即动态数组,是最常用的 List 实现类,内部是通过数组实现的,它允许对元素进行快速随机访问。用 MSDN(Microsoft Developer Network,微软开发者网络)中的说法,就是 Array 的复杂版本。

ArrayList 底层使用数组实现,由于数组的缺点是每个元素之间不能有间隔,当数组大小不满足时需要增加存储能力,就要将已经有数组的数据复制到新的存储空间中。所以当从 ArrayList 的中间位置插入或者删除元素时,需要对数组进行复制、移动、代价比较高。因此,它适合随机查找和遍历,不适合插入和删除。

ArrayList 的每个实例都有一个容量,该容量是指用来存储列表元素的数组的大小。它总是大于等于列表的大小。随着向ArrayList 中不断添加元素,其容量也自动增长。自动增长会带来数据向新数组的重新拷贝,因此,如果可预知数据量的多少,可在构造ArrayList 时指定其容量。此外,ArrayList 在被添加大量元素前,应用程序也可以使用 ensureCapacity() 操作来指定ArrayList 实例的容量,这可以减少递增式再分配的数量。

ArrayList 是非线程安全的,只能在单线程环境下使用,多线程环境下可以考虑用 (List l) 函数返回一个线程安全的 ArrayList 类,也可以使用 并发包下的 CopyOnWriteArrayList 类。

1.2、ArrayList 特点

ArrayList 是一个可变大小的数组,可以动态地调整大小。ArrayList 有以下特点:

  1. 动态数组:ArrayList 底层是一个动态数组,能够自动调整其大小。当添加新元素时,如果内部数组空间不足,ArrayList 会创建一个更大的数组并将旧数组中的元素复制到新数组中;
  2. 有序:ArrayList 保持元素的插入顺序,即按照元素添加到集合中的顺序来存储它们;
  3. 快速随机访问:由于底层是数组,可以通过索引快速访问元素,时间复杂度为 O(1)
  4. 可变大小:ArrayList 的大小是动态调整的,用户不需要指定其初始大小,但可以通过构造函数指定初始容量;
  5. 不保证线程安全:ArrayList 不是线程安全的。如果多个线程同时访问一个 ArrayList 实例,并且至少一个线程修改了列表的结构,那么必须在外部实现同步;
  6. 允许 null 元素:ArrayList 允许存储 null 元素;
  7. 频繁的增删操作效率较低:由于插入或删除元素时需要移动数组中的其他元素,频繁的插入和删除操作会导致性能下降,特别是在列表的中间位置进行操作时,时间复杂度为 O(n)
1.3、ArrayList 使用用例

ArrayList 是 Java 中广泛使用的集合类,适用于各种场景。以下是常见的使用用例和代码示例,创建一个 ArrayList,并进行基本的增删查操作::

import java.util.ArrayList;

public class ArrayListBasicOperations {
    public static void main(String[] args) {
        // 创建一个 ArrayList
        ArrayList<String> fruits = new ArrayList<>();

        // 添加元素
        fruits.add("Apple");
        fruits.add("Banana");
        fruits.add("Cherry");

        // 获取元素
        String fruit = fruits.get(1); // "Banana"
        System.out.println("The second fruit is: " + fruit);

        // 删除元素
        fruits.remove("Apple");

        // 遍历列表
        for (String item : fruits) {
            System.out.println(item);
        }

        // 检查列表大小
        int size = fruits.size();
        System.out.println("List size: " + size);
    }
}

1.4、ArrayList 实现原理

ArrayList 是 Java 中基于动态数组实现的集合类,其实现原理主要包括以下几个方面:数据结构、扩容机制、添加和删除元素的过程,以及一些常见操作的时间复杂度。

  1. 数据结构:ArrayList 底层使用一个数组来存储元素。它定义了一个初始容量,当元素数量超过当前容量时,会自动扩容;
  2. 扩容机制:当向 ArrayList 中添加元素且当前数组已满时,会触发扩容机制。扩容时,新的数组容量通常是旧容量的 1.5 倍;
  3. 添加元素:添加元素有两种方式:在末尾添加和在指定位置添加。前者是最常见的操作,时间复杂度为 O(1);后者需要移动指定位置之后的元素,时间复杂度为 O(n)
  4. 删除元素:删除元素同样有两种方式:删除指定位置的元素和删除指定元素的第一次出现。删除指定位置的元素需要移动后面的元素,时间复杂度为 O(n);删除指定元素的第一次出现则需要先查找,时间复杂度为 O(n)
1.5、ArrayList 优缺点及适用场景

优点:

  1. 动态调整大小:ArrayList 可以根据需要动态调整其大小,用户不需要手动管理数组的大小;
  2. 快速随机访问:由于底层是数组,ArrayList 支持快速的随机访问,时间复杂度为 O(1);
  3. 有序:ArrayList 保持元素的插入顺序,这使得它非常适合需要保持顺序的场景;
  4. 实现简单:ArrayList 的实现相对简单,提供了丰富的 API,使用起来非常方便;
  5. 允许重复和 null 元素:ArrayList 允许存储重复元素和 null 元素,增加了灵活;
  6. 扩容机制:ArrayList 内部的扩容机制能够有效减少扩容次数,提升性能。

缺点:

  1. 插入和删除效率低:在 ArrayList 中间插入或删除元素时,需要移动数组中的其他元素,时间复杂度为 O(n),因此在频繁插入和删除操作的场景中效率较低;
  2. 非线程安全:ArrayList 不是线程安全的,如果在多线程环境中使用,需要手动进行同步,或者使用 包装;
  3. 占用额外内存:ArrayList 在扩容时需要分配新的数组并复制旧数组中的元素,这会占用额外的内存,并可能导致内存碎片问题;
  4. 空间浪费:当 ArrayList 的实际元素数量远小于其容量时,会浪费内存空间;
  5. 扩容时性能开销:当 ArrayList 扩容时,需要进行数组复制操作,会导致短暂的性能下降,特别是在大规模扩容时。

适用场景:

  • 快速随机访问:如果应用程序需要频繁地通过索引访问元素,ArrayList 是一个很好的选择;
  • 读取多、写入少:适用于读取操作多于写入操作的场景;
  • 存储有序数据:适用于需要保持元素插入顺序的场景。

不适用场景:

  • 频繁插入和删除:不适用于频繁在中间位置插入或删除元素的场景,因为这会导致大量的数组移动操作;
  • 线程安全要求:不适用于需要线程安全的场景,除非使用同步包装。

2、ArrayList 源码

ArrayList 通过动态数组来实现,其扩容机制保证了在添加大量元素时能够自动调整容量。它提供了快速的随机访问,但在进行大量插入和删除操作时效率较低,特别是在列表中间进行操作时。这些特性使得 ArrayList 适用于需要频繁访问元素但插入和删除操作较少的场景。

为了更好的理解 ArrayList 的这些特性,接下来我们以 JDK1.8 中的代码为例,看一下它的源码!

2.1、数据结构

ArrayList 通过维护一个数组 elementData 来存储其元素,并使用两个共享的空数组实例 EMPTY_ELEMENTDATADEFAULTCAPACITY_EMPTY_ELEMENTDATA 来区分不同状态下的空 ArrayList。当添加第一个元素时,ArrayList 会根据当前状态选择合适的扩展策略。这个设计使得 ArrayList 能够高效地管理其内部数组,在需要时进行动态扩展。

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
    // 省略其他方法和实现细节
    ...
      
    /**
     * 用于空实例的共享空数组实例。
     */
    private static final Object[] EMPTY_ELEMENTDATA = {};

    /**
     * 用于默认大小空实例的共享空数组实例。
     * 我们将其与 EMPTY_ELEMENTDATA 区分开来,以便在添加第一个元素时知道要扩充多少。
     */
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

    /**
     * 存储 ArrayList 元素的数组缓冲区。
     * ArrayList 的容量即为此数组缓冲区的长度。
     * 任何 elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA 的空 ArrayList 在添加第一个元素时会扩展到 DEFAULT_CAPACITY。
     */
    transient Object[] elementData; // 为简化嵌套类访问,非私有

    /**
     * ArrayList 的大小(它包含的元素数量)。
     *
     * @serial
     */
    private int size;

    // 省略其他方法和实现细节
    ...
}
2.2、构造方法

ArrayList 提供了灵活的构造方法来初始化列表,可以指定初始容量、使用默认容量或通过集合初始化。通过这些构造方法,ArrayList 能够满足不同场景下的初始化需求,并且在初始化时根据传入参数的不同进行相应的处理和优化。

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
    // 省略其他方法和实现细节
    ...
      
    /**
     * 使用指定的初始容量构造一个空列表。
     *
     * @param  initialCapacity  列表的初始容量
     * @throws IllegalArgumentException 如果指定的初始容量为负数
     */
    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);
        }
    }

    /**
     * 构造一个初始容量为十的空列表。
     */
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

    /**
     * 构造一个包含指定集合元素的列表,按集合的迭代器返回的顺序。
     *
     * @param c 包含要放入此列表中的元素的集合
     * @throws NullPointerException 如果指定的集合为 null
     */
    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 {
            // 用空数组替换。
            elementData = EMPTY_ELEMENTDATA;
        }
    }

    // 省略其他方法和实现细节
    ...
}

2.3、元素访问

正因为 ArrayList 的底层实现是数组(一块连续的内存区域),所以可以轻松的实现快速的随机访问和按索引操作。get 方法在访问或修改元素之前都会调用 rangeCheck 方法检查索引的有效性,而 rangeCheck 方法会在索引超出范围时抛出详细的错误信息。

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
    // 省略其他方法和实现细节
    ...
    
    /**
     * 在指定索引处获取元素。
     *
     * @param index 要获取元素的位置
     * @return 指定索引处的元素
     */
    E elementData(int index) {
        return (E) elementData[index];
    }

    /**
     * 获取指定索引处的元素,并进行范围检查。
     *
     * @param index 要获取元素的位置
     * @return 指定索引处的元素
     * @throws IndexOutOfBoundsException 如果索引超出范围
     */
    public E get(int index) {
        rangeCheck(index); // 检查索引是否在范围内
        return elementData(index); // 返回指定索引处的元素
    }

    /**
     * 检查索引是否在范围内。
     *
     * @param index 要检查的索引
     * @throws IndexOutOfBoundsException 如果索引超出范围
     */
    private void rangeCheck(int index) {
        if (index >= size || index < 0) {
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
        }
    }

    /**
     * 构建索引超出范围的错误信息。
     *
     * @param index 超出范围的索引
     * @return 错误信息字符串
     */
    private String outOfBoundsMsg(int index) {
        return "Index: " + index + ", Size: " + size;
    }
  
    // 省略其他方法和实现细节
    ...
}
2.4、添加操作

我们知道,数组需要使用着一块连续的内存空间,因此数组的大小一旦被规定就无法改变。那如果我们不断的往里面添加数据的话,ArrayList 是如何进行扩容的呢 ?答案就是 ArrayList 会复制一个新的更大容量的数组:ArrayList 在添加元素时会先调用名为 ensureCapacityInternal(int minCapacity) 的方法,对数组容量进行检查,判断剩余空间是否足够,不够时则进行扩容。

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{

  	// 省略其他方法和实现细节
  	...
    
    /**
     * 在 ArrayList 末尾添加一个元素。
     *
     * @param e 要添加的元素
     * @return 总是返回 true
     */
    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // 确保容量足够容纳新元素,增加 modCount
        elementData[size++] = e;           // 将新元素添加到数组中,size 自增
        return true;
    }

    /**
     * 确保 ArrayList 的容量足够容纳指定数量的元素。
     *
     * @param minCapacity 需要的最小容量
     */
    private void ensureCapacityInternal(int minCapacity) {
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }

    /**
     * 计算所需的容量
     *
     * @param elementData 当前的数组
     * @param minCapacity 需要的最小容量
     * @return 计算后的容量
     */
    private static int calculateCapacity(Object[] elementData, int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        return minCapacity;
    }

    /**
     * 显式地确保 ArrayList 的容量足够。
     *
     * @param minCapacity 需要的最小容量
     */
    private void ensureExplicitCapacity(int minCapacity) {
      // 递增修改计数,以支持快速失败机制
        modCount++;  

        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);  // 如果所需容量超过当前数组长度,则扩展数组
    }

    /**
     * 扩展数组的容量。
     *
     * @param minCapacity 需要的最小容量
     */
    private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        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);
    }
  
  	// 省略其他方法和实现细节
  	...
}
2.5、扩容机制

扩容后的数组大小是原大小的 1.5 倍(oldCapacity + (oldCapacity >> 1))。最后将旧数组进行复制(调用 (),再调用 () ),达到扩容的目的,此时新旧列表的 size 大小相同,但 elementData 的长度即容量不同。

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{

  	// 省略其他方法和实现细节
  	...

    /**
     * 扩展数组的容量。
     *
     * @param minCapacity 需要的最小容量
     */
    private void grow(int minCapacity) {
        // 将数组长度赋值给 oldCapacity
        int oldCapacity = elementData.length;
    		// 将 oldCapacity 右移一位再加上 oldCapacity,即相当于 newCapacity=1.5 倍 oldCapacity(不考虑精度损失)
        int newCapacity = oldCapacity + (oldCapacity >> 1);
    		// 如果 newCapacity 还是小于 minCapacity,直接将 minCapacity 赋值给 newCapacity
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
    		// 特殊情况:newCapacity 的值过大,直接将整型最大值赋给 newCapacity,
				// 即 newCapacity=Integer.MAX_VALUE
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // 将 elementData 的数据拷贝到扩容后的数组
        elementData = Arrays.copyOf(elementData, newCapacity);
    }
  
    /`
     * 如果大于临界值,进行整型最大值的分配
     *
     * @param minCapacity 需要的最小容量
     */
		private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) {
        // overflow
            throw new OutOfMemoryError();
         }
        return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE;
		}
  
  	// 省略其他方法和实现细节
  	...
}
2.6、删除操作

ArrayList 在进行根据索引删除时,通过系统 native 方法 完成的,原理就是将删除位置后的所有元素重新复制到删除位置到原来最后元素的前一位,并使用 elementData[--size] = null; 清空多余的值。

package java.util;

import ...
  
public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{

  	// 省略其他方法和实现细节
  	...
    
    /**
     * 从列表中移除指定位置的元素。
     * 将所有后续元素左移(索引减一)。
     *
     * @param index 要移除元素的索引位置
     * @return 被移除的元素
     * @throws IndexOutOfBoundsException 如果索引超出范围抛出此异常
     */
    public E remove(int index) {
        rangeCheck(index); // 检查索引是否在合法范围内,防止索引越界

        modCount++; // 修改次数加一,用于实现迭代器的快速失败机制(在迭代过程中如果检测到结构变化会抛出异常)
        E oldValue = elementData(index); // 获取并保存将要被移除的元素值

        int numMoved = size - index - 1; // 计算从 index 之后需要左移的元素数量
        if (numMoved > 0)
            System.arraycopy(elementData, index + 1, elementData, index,
                    numMoved); // 使用系统方法 arraycopy 来高效地移动数组元素
        elementData[--size] = null; // 将最后一个元素设为 null,帮助垃圾收集器回收,并减少实际元素数量

        return oldValue; // 返回被移除的元素
    }

    /**
     * 从列表中移除第一次出现的指定元素(如果存在)。
     * 如果列表不包含该元素,则不做改变。
     * 更正式地说,移除满足 (o==null ? get(i)==null : (get(i))) 条件的最低索引的元素。
     *
     * @param o 要从列表中移除的元素
     * @return 如果列表包含指定元素则返回 true
     */
    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; // 如果没有找到元素,返回 false
    }

    /**
     * 一个私有的快速移除方法,不进行边界检查也不返回被移除的值。
     * 用于内部操作,以提高性能。
     */
    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; // 清理引用,帮助垃圾回收
    }

  
  	// 省略其他方法和实现细节
  	...
}

3、ArrayList 知识点补充

3.1、ArrayList 方法概述

ArrayList 类提供了多种方法,用于元素的添加、删除、访问、修改等操作。以下是 ArrayList 类的方法列表,并标注了每个方法的来源(自带、接口、父类)和参数说明。

  • add(E e):实现 (List),添加元素到列表末尾,返回值类型为 boolean
  • add(int index, E element):实现 (List),在指定位置插入元素,无返回值;
  • addAll(Collection<? extends E> c):实现 (List),添加集合中的所有元素,返回值类型为 boolean
  • addAll(int index, Collection<? extends E> c):实现 (List),在指定位置插入集合中的所有元素,返回值类型为 boolean
  • clear():自带,清空列表,无返回值;
  • clone():自带,克隆列表,返回值类型为 Object
  • contains(Object o):实现 (Collection),检查列表是否包含指定元素,返回值类型为 boolean
  • ensureCapacity(int minCapacity):自带,确保列表的最小容量,无返回值;
  • forEach(Consumer<? super E> action):实现 (Iterable),对列表中的每个元素执行指定操作,无返回值;
  • get(int index):自带,获取指定位置的元素,返回值类型为 E
  • indexOf(Object o):自带,返回指定元素第一次出现的位置,返回值类型为 int
  • isEmpty():实现 (Collection),检查列表是否为空,返回值类型为 boolean
  • iterator():实现 (Iterable),返回列表的迭代器,返回值类型为 Iterator<E>
  • lastIndexOf(Object o):自带,返回指定元素最后一次出现的位置,返回值类型为 int
  • listIterator():自带,返回列表的列表迭代器,返回值类型为 ListIterator<E>
  • listIterator(int index):自带,返回从指定位置开始的列表迭代器,返回值类型为 ListIterator<E>
  • remove(int index):自带,移除指定位置的元素,返回值类型为 E
  • remove(Object o):自带,移除首次出现的指定元素,返回值类型为 boolean
  • removeAll(Collection<?> c):自带,移除所有包含在指定集合中的元素,返回值类型为 boolean
  • removeIf(Predicate<? super E> filter):实现 (Collection),移除满足条件的元素,返回值类型为 boolean
  • replaceAll(UnaryOperator<E> operator):实现 (List),替换每个元素,无返回值;
  • retainAll(Collection<?> c):自带,保留所有包含在指定集合中的元素,返回值类型为 boolean
  • set(int index, E element):自带,替换指定位置的元素,返回值类型为 E
  • size():自带,返回列表的大小,返回值类型为 int
  • sort(Comparator<? super E> c):自带,排序列表,无返回值;
  • spliterator():实现 (Collection),返回列表的可拆分迭代器,返回值类型为 Spliterator<E>
  • subList(int fromIndex, int toIndex):自带,返回指定范围的子列表,返回值类型为 List<E>
  • toArray():实现 (Collection),将列表转换为数组,返回值类型为 Object[]
  • toArray(T[] a):实现 (Collection),将列表转换为指定类型的数组,返回值类型为 T[]
  • trimToSize():自带,修剪列表的容量为当前大小,无返回值。
3.2、在遍历 ArrayList 时移除元素时的问题

在遍历 ArrayList 或其他类型的 List 集合时进行元素的删除,确实需要格外小心。这是因为集合的结构修改(如添加或删除元素)可能会影响遍历过程,导致 ConcurrentModificationException 异常或者跳过元素。这里详细解释下这两种情况:

3.2.1、ConcurrentModificationException

ConcurrentModificationException 是 Java 在迭代器检测到除了它自身的 remove() 方法之外的任何修改时抛出的异常。这是 Java 集合框架的 “fail-fast” 行为的一部分,用来快速响应错误,防止未定义的行为。

当你使用迭代器或者增强型 for 循环(本质上使用的是迭代器)遍历 ArrayList 时,如果在遍历过程中通过集合的直接方法(如 (index))修改集合,就会导致迭代器的内部状态与集合的状态不一致。因为迭代器内部有一个 expectedModCount 变量,用来记录初始化迭代器时集合的修改次数。每次迭代时,迭代器都会检查集合的当前修改次数是否仍然等于 expectedModCount。如果不等,就会抛出 ConcurrentModificationException

public class Main {
    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        list.add(1);
        list.add(2);
        list.add(3);
        list.add(4);
        list.add(5);

        for (Integer number : list) {
            if (number % 2 == 0) {
                list.remove(number);  // 尝试删除元素
            }
        }
    }
}

在这个例子中,尝试删除所有偶数。因为 (Object) 修改了集合,而这种修改没有通过迭代器进行,所以当迭代器检测到修改时(在下一次调用 next() 时),它将抛出 ConcurrentModificationException

3.2.2、跳过元素

另一个常见的问题是在使用普通的 for 循环遍历并同时删除元素时可能会跳过某些元素。这是因为删除元素会影响数组的索引:

import java.util.ArrayList;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        list.add(1);
        list.add(2);
        list.add(3);
        list.add(4);
        list.add(5);

        for (int i = 0; i < list.size(); i++) {
            if (list.get(i) % 2 == 0) {
                list.remove(i);  // 删除元素
                i--;  // 通过递减 i 来调整索引
            }
        }
    }
}

在这个例子中,我们尝试删除所有偶数。如果不递减 i 的值,当元素被移除时,列表中的元素会向左移动,这导致下一个元素跳过检查。在上面的代码中,通过 i-- 调整,我们倒是可以确保即使删除了元素,也不会跳过任何元素。

3.3、遍历 ArrayList 时移除元素时的问题解决方案
3.3.1、使用迭代器的 remove 方法

当使用迭代器直接遍历时,应该使用迭代器自身的 remove() 方法来删除元素。这样可以安全地更新 modCountexpectedModCount,防止 ConcurrentModificationException

Iterator<Integer> it = list.iterator();
while (it.hasNext()) {
    Integer value = it.next();
    if (value % 2 == 0) {
        it.remove();  // 安全删除
    }
}
3.3.2、使用 Java8 的 removeIf 方法

Java8 提供的 removeIf() 方法可以在内部安全地修改列表,并避免 ConcurrentModificationException

list.removeIf(n -> n % 2 == 0); // 删除所有偶数
3.3.3、使用逆向遍历

使用传统的 for 循环时,从列表的尾部向前遍历和删除可以避免跳过元素的问题:

for (int i = list.size() - 1; i >= 0; i--) {
    if (list.get(i) % 2 == 0) {
        list.remove(i);
    }
}

这些方法各有优势和适用场景。选择哪种方法取决于具体的需求和环境。在多数情况下,使用迭代器的 remove() 方法或 removeIf() 方法提供了最简洁和安全的解决方案。