欢迎光临
我们一直在努力

Java 集合框架进阶——List 实现类深度解析与实战优化

第二十四篇:Java 集合框架进阶——List 实现类深度解析与实战优化

在这里插入图片描述

一、章节学习目标与重点

1.1 学习目标

  • 深入理解 ArrayList、LinkedList、Vector 三大 List 实现类的底层数据结构与设计原理
  • 掌握各 List 实现类的核心方法源码逻辑,明确其性能差异的根源
  • 能够根据实际业务场景选择合适的 List 实现类,并进行针对性优化
  • 熟练解决 List 使用过程中的常见问题(如并发安全、扩容损耗、遍历效率等)

1.2 学习重点

  • ArrayList 扩容机制与索引操作的性能优化
  • LinkedList 双向链表结构与增删操作的底层实现
  • Vector 与 ArrayList 的区别及并发场景下的正确使用
  • List 遍历方式的效率对比与选择策略
  • 大量数据处理时 List 的内存优化技巧

二、List 接口核心特性回顾

💡 List 作为 Java 集合框架中最常用的接口之一,继承自 Collection 接口,其核心特性是有序性和可重复性:

  • 有序性:元素的插入顺序与遍历顺序保持一致,支持通过索引(index)直接访问元素
  • 可重复性:允许存储多个相等的元素(equals() 方法返回 true)
  • 核心方法:除了继承自 Collection 的 add()、remove()、size() 等方法外,List 新增了基于索引的操作方法,如 get(int index)、set(int index, E element)、add(int index, E element) 等

// List 接口核心方法示例
public interface List<E> extends Collection<E> {
// 基于索引获取元素
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);
// 获取 List 迭代器
ListIterator<E> listIterator();
}

⚠️ 注意:List 接口本身是抽象的,无法直接实例化,必须通过其实现类创建对象。Java 中最常用的 List 实现类有三个:ArrayList、LinkedList、Vector,它们各自基于不同的底层数据结构,适用于不同的业务场景。

三、ArrayList 深度解析:动态数组的设计与优化

3.1 底层数据结构

💡 ArrayList 的底层是动态数组(Object[]),通过数组扩容机制实现容量的动态增长。其核心成员变量如下:

public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
// 存储元素的数组
transient Object[] elementData;
// 集合中实际元素的个数
private int size;
// 默认初始容量
private static final int DEFAULT_CAPACITY = 10;
// 空数组(用于空实例)
private static final Object[] EMPTY_ELEMENTDATA = {};
// 默认空数组(用于默认构造函数)
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
}

  • 当使用无参构造函数创建 ArrayList 时,初始 elementData 为 DEFAULTCAPACITY_EMPTY_ELEMENTDATA,首次添加元素时才会初始化容量为 10
  • 当使用指定容量的构造函数时,若指定容量大于 0,则初始化 elementData 为对应容量的数组;若等于 0,则使用 EMPTY_ELEMENTDATA

3.2 核心方法源码解析

3.2.1 add() 方法:元素添加与扩容机制

ArrayList 的 add() 方法有两个重载版本:末尾添加和指定索引添加,核心逻辑是先检查容量,不足则扩容,再添加元素。

(1)末尾添加元素:add(E e)

public boolean add(E e) {
// 确保容量充足(size + 1)
ensureCapacityInternal(size + 1);
// 元素添加到数组末尾,size 自增
elementData[size++] = e;
return true;
}

// 确保内部容量
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

// 计算所需容量
private static int calculateCapacity(Object[] elementData, int minCapacity) {
// 若为默认空数组,返回默认容量(10)与最小所需容量的最大值
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;
// 新容量 = 旧容量 + 旧容量/2(即扩容 1.5 倍)
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 若新容量仍小于所需容量,直接使用所需容量
if (newCapacity minCapacity < 0) {
newCapacity = minCapacity;
}
// 若新容量超过最大容量限制(Integer.MAX_VALUE – 8),则使用 Integer.MAX_VALUE
if (newCapacity MAX_ARRAY_SIZE > 0) {
newCapacity = hugeCapacity(minCapacity);
}
// 复制原数组元素到新数组(Arrays.copyOf 底层调用 System.arraycopy,native 方法,效率较高)
elementData = Arrays.copyOf(elementData, newCapacity);
}

💡 扩容机制关键点:

  • 初始容量为 10,每次扩容为原容量的 1.5 倍(通过位运算 oldCapacity >> 1 实现,效率高于除法)
  • 扩容时会创建新数组,并将原数组元素复制到新数组,这个过程会消耗一定的时间和内存,因此建议在创建 ArrayList 时,根据预期元素数量指定初始容量,减少扩容次数
  • 最大容量限制为 Integer.MAX_VALUE – 8,主要是为了避免某些虚拟机在数组头存储额外信息导致的溢出问题
  • (2)指定索引添加元素:add(int index, E element)

    public void add(int index, E element) {
    // 检查索引是否越界(0 <= index <= size)
    rangeCheckForAdd(index);
    // 确保容量充足
    ensureCapacityInternal(size + 1);
    // 复制原数组中 index 及之后的元素,向后移动一位(腾出 index 位置)
    System.arraycopy(elementData, index, elementData, index + 1, size index);
    // 在 index 位置插入新元素
    elementData[index] = element;
    size++;
    }

    // 索引越界检查
    private void rangeCheckForAdd(int index) {
    if (index > size || index < 0) {
    throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }
    }

    ⚠️ 注意:指定索引添加元素时,需要移动数组元素,时间复杂度为 O(n),元素越多,效率越低。因此,ArrayList 不适合频繁在中间位置插入元素的场景。

    3.2.2 get() 方法:索引访问的高效实现

    public E get(int index) {
    // 索引越界检查
    rangeCheck(index);
    // 直接通过数组索引获取元素,时间复杂度 O(1)
    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];
    }

    💡 由于 ArrayList 底层是数组,数组支持随机访问,因此 get() 方法直接通过索引获取元素,效率极高,这是 ArrayList 最核心的优势之一。

    3.2.3 remove() 方法:元素删除与数组收缩

    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);
    }
    // 释放最后一个元素的引用(帮助 GC)
    elementData[size] = null;
    // 返回被删除的元素
    return oldValue;
    }

    ⚠️ 注意:删除非末尾元素时,需要移动数组元素,时间复杂度为 O(n);同时,ArrayList 删除元素后不会自动收缩数组容量(即 elementData 长度不变),只会减少 size 计数,若需要释放内存,可手动调用 trimToSize() 方法。

    // 手动收缩数组容量,释放空闲内存
    public void trimToSize() {
    modCount++;
    if (size < elementData.length) {
    elementData = (size == 0) ? EMPTY_ELEMENTDATA : Arrays.copyOf(elementData, size);
    }
    }

    3.3 ArrayList 性能分析与使用场景

    3.3.1 性能特点
    操作类型时间复杂度说明
    末尾添加(add(E e)) O(1) 扩容时为 O(n),但扩容频率低(1.5 倍扩容)
    指定索引添加/删除 O(n) 需要移动数组元素
    索引访问(get/set) O(1) 随机访问,效率极高
    查找元素(indexOf) O(n) 需遍历数组
    3.3.2 适用场景
    • 频繁进行索引访问(get/set 操作)的场景
    • 元素添加/删除主要在集合末尾的场景
    • 不需要线程安全的场景(ArrayList 是非线程安全的)
    3.3.3 性能优化技巧

    💡 技巧 1:初始化时指定初始容量,减少扩容次数

    // 推荐:已知元素数量约为 1000,直接指定初始容量
    List<String> list = new ArrayList<>(1000);
    // 不推荐:无参构造,首次添加元素扩容为 10,后续需多次扩容
    List<String> list = new ArrayList<>();

    💡 技巧 2:避免在中间位置频繁添加/删除元素,若需频繁修改,考虑使用 LinkedList

    💡 技巧 3:遍历 ArrayList 时,优先使用普通 for 循环或增强 for 循环(foreach),效率高于迭代器(Iterator)

    // 高效遍历方式
    ArrayList<String> list = new ArrayList<>();
    // 普通 for 循环(索引访问,O(1))
    for (int i = 0; i < list.size(); i++) {
    String element = list.get(i);
    }
    // 增强 for 循环(底层基于迭代器,但语法简洁,效率接近普通 for 循环)
    for (String element : list) {
    // 业务逻辑
    }

    💡 技巧 4:大量数据处理后,若集合不再添加元素,调用 trimToSize() 释放空闲内存

    List<LargeObject> list = new ArrayList<>(10000);
    // 批量添加大量元素…
    list.trimToSize(); // 收缩数组容量,释放未使用的内存

    四、LinkedList 深度解析:双向链表的设计与实现

    4.1 底层数据结构

    💡 LinkedList 的底层是双向链表,每个节点(Node)包含前驱节点引用、后继节点引用和元素值。其核心成员变量如下:

    public class LinkedList<E> extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable {
    // 链表长度
    transient int size = 0;
    // 头节点(前驱为 null)
    transient Node<E> first;
    // 尾节点(后继为 null)
    transient Node<E> last;

    // 双向链表节点类
    private static class Node<E> {
    E item; // 元素值
    Node<E> next; // 后继节点引用
    Node<E> prev; // 前驱节点引用

    Node(Node<E> prev, E element, Node<E> next) {
    this.prev = prev;
    this.item = element;
    this.next = next;
    }
    }
    }

    双向链表结构示意图:

    first(头节点) last(尾节点)
    ↓ ↓
    [prev:null, item:A, next→] ←→ [prev←, item:B, next→] ←→ … ←→ [prev←, item:Z, next:null]

    • 头节点的 prev 为 null,尾节点的 next 为 null
    • 每个节点都能通过 prev 和 next 引用访问前后节点,实现双向遍历
    • 与 ArrayList 不同,LinkedList 不需要数组扩容,因为链表节点可以动态创建

    4.2 核心方法源码解析

    4.2.1 add() 方法:节点添加机制
    (1)末尾添加元素:add(E e)

    public boolean add(E e) {
    // 调用 linkLast 方法,将元素链接到链表尾部
    linkLast(e);
    return true;
    }

    // 链接到链表尾部
    void linkLast(E e) {
    // 保存当前尾节点
    final Node<E> l = last;
    // 创建新节点(前驱为当前尾节点,后继为 null)
    final Node<E> newNode = new Node<>(l, e, null);
    // 更新尾节点为新节点
    last = newNode;
    // 若原链表为空(l 为 null),则头节点也指向新节点
    if (l == null) {
    first = newNode;
    } else {
    // 原尾节点的后继指向新节点
    l.next = newNode;
    }
    // 链表长度自增
    size++;
    modCount++;
    }

    💡 末尾添加元素时,只需创建新节点并更新尾节点引用,时间复杂度为 O(1),无需移动元素,效率极高。

    (2)指定索引添加元素:add(int index, E element)

    public void add(int index, E element) {
    // 检查索引是否越界(0 <= index <= size)
    checkPositionIndex(index);
    // 若索引等于 size,直接添加到尾部
    if (index == size) {
    linkLast(element);
    } else {
    // 找到索引位置的节点,将新节点链接到其前面
    linkBefore(element, node(index));
    }
    }

    // 找到指定索引位置的节点
    Node<E> node(int index) {
    // 优化:根据索引位置判断从头部还是尾部遍历(二分思想)
    if (index < (size >> 1)) {
    // 索引在前半部分,从头部遍历
    Node<E> x = first;
    for (int i = 0; i < index; i++) {
    x = x.next;
    }
    return x;
    } else {
    // 索引在后半部分,从尾部遍历
    Node<E> x = last;
    for (int i = size 1; i > index; i) {
    x = x.prev;
    }
    return x;
    }
    }

    // 将新节点链接到指定节点之前
    void linkBefore(E e, Node<E> succ) {
    // 保存指定节点的前驱节点
    final Node<E> pred = succ.prev;
    // 创建新节点(前驱为 pred,后继为 succ)
    final Node<E> newNode = new Node<>(pred, e, succ);
    // 指定节点的前驱指向新节点
    succ.prev = newNode;
    // 若 pred 为 null(说明 succ 是头节点),则头节点更新为新节点
    if (pred == null) {
    first = newNode;
    } else {
    // pred 的后继指向新节点
    pred.next = newNode;
    }
    size++;
    modCount++;
    }

    💡 指定索引添加元素的核心逻辑:

  • 先通过 node() 方法找到索引位置的节点,该方法通过二分思想优化遍历效率,时间复杂度为 O(n/2) = O(n)
  • 再通过 linkBefore() 方法插入新节点,仅需修改前后节点的引用,时间复杂度为 O(1)
  • 整体时间复杂度为 O(n),但比 ArrayList 在中间位置添加元素的效率高(ArrayList 需移动数组元素,LinkedList 仅需修改引用)
  • 4.2.2 get() 方法:索引访问的实现

    public E get(int index) {
    // 检查索引是否越界
    checkElementIndex(index);
    // 调用 node() 方法找到指定索引的节点,返回元素值
    return node(index).item;
    }

    ⚠️ 注意:LinkedList 没有数组那样的随机访问能力,get() 方法需要通过 node() 方法遍历链表找到目标节点,时间复杂度为 O(n),因此索引访问效率远低于 ArrayList。

    4.2.3 remove() 方法:节点删除机制

    public E remove(int index) {
    // 检查索引是否越界
    checkElementIndex(index);
    // 找到指定索引的节点,调用 unlink 方法删除
    return unlink(node(index));
    }

    // 删除指定节点
    E unlink(Node<E> x) {
    // 获取节点的元素值、前驱节点、后继节点
    final E element = x.item;
    final Node<E> next = x.next;
    final Node<E> prev = x.prev;

    // 处理前驱节点
    if (prev == null) {
    // 若前驱为 null,说明 x 是头节点,更新头节点为 next
    first = next;
    } else {
    // 前驱节点的后继指向 next,断开与 x 的连接
    prev.next = next;
    x.prev = null; // 帮助 GC
    }

    // 处理后继节点
    if (next == null) {
    // 若后继为 null,说明 x 是尾节点,更新尾节点为 prev
    last = prev;
    } else {
    // 后继节点的前驱指向 prev,断开与 x 的连接
    next.prev = prev;
    x.next = null; // 帮助 GC
    }

    // 清空节点的元素值,帮助 GC
    x.item = null;
    size;
    modCount++;
    return element;
    }

    💡 删除节点时,仅需修改前后节点的引用,断开与目标节点的连接,时间复杂度为 O(1)(找到目标节点的时间复杂度为 O(n)),因此 LinkedList 适合频繁添加/删除元素的场景。

    4.3 LinkedList 性能分析与使用场景

    4.3.1 性能特点
    操作类型时间复杂度说明
    末尾/头部添加/删除 O(1) 直接修改头尾节点引用
    指定索引添加/删除 O(n) 遍历找到目标节点(O(n))+ 修改引用(O(1))
    索引访问(get/set) O(n) 需遍历链表
    查找元素(indexOf) O(n) 需遍历链表
    4.3.2 适用场景
    • 频繁在集合中间位置添加/删除元素的场景
    • 元素添加/删除主要在头部或尾部的场景(LinkedList 实现了 Deque 接口,可作为双端队列使用)
    • 不需要频繁进行索引访问的场景
    4.3.3 LinkedList 与 ArrayList 核心区别对比
    特性ArrayListLinkedList
    底层结构 动态数组 双向链表
    索引访问效率 O(1)(高效) O(n)(低效)
    中间位置增删效率 O(n)(低效,需移动元素) O(n)(较高效,仅需遍历+修改引用)
    末尾增删效率 O(1)(扩容时 O(n)) O(1)(稳定高效)
    内存占用 连续内存,可能有空闲空间 非连续内存,每个节点额外存储前后引用
    线程安全 非线程安全 非线程安全
    实现接口 List、RandomAccess List、Deque、Queue

    4.4 LinkedList 实用技巧

    💡 技巧 1:利用 LinkedList 实现双端队列(Deque)功能

    // 作为双端队列使用
    Deque<String> deque = new LinkedList<>();
    deque.addFirst("A"); // 头部添加
    deque.addLast("B"); // 尾部添加
    String first = deque.getFirst(); // 获取头部元素
    String last = deque.getLast(); // 获取尾部元素
    deque.removeFirst(); // 删除头部元素
    deque.removeLast(); // 删除尾部元素

    💡 技巧 2:遍历 LinkedList 时,优先使用迭代器(Iterator)或增强 for 循环,避免使用普通 for 循环(频繁调用 get() 方法,效率极低)

    LinkedList<String> list = new LinkedList<>();
    // 推荐:增强 for 循环(底层基于迭代器,效率高)
    for (String element : list) {
    // 业务逻辑
    }
    // 推荐:迭代器遍历
    Iterator<String> iterator = list.iterator();
    while (iterator.hasNext()) {
    String element = iterator.next();
    // 业务逻辑
    }
    // 不推荐:普通 for 循环(每次 get(i) 都要遍历链表)
    for (int i = 0; i < list.size(); i++) {
    String element = list.get(i); // 效率极低,O(n²) 时间复杂度
    }

    五、Vector 解析:线程安全的动态数组

    5.1 底层数据结构与核心特性

    💡 Vector 是 Java 早期提供的 List 实现类,底层同样是动态数组(Object[]),与 ArrayList 结构相似,但核心区别是线程安全——Vector 的所有核心方法都被 synchronized 关键字修饰,确保多线程环境下的操作原子性。

    public class Vector<E> extends AbstractList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
    // 存储元素的数组
    protected Object[] elementData;
    // 集合中实际元素的个数
    protected int elementCount;
    // 扩容增量(默认 0)
    protected int capacityIncrement;
    // 默认初始容量
    private static final int DEFAULT_CAPACITY = 10;
    }

    5.2 核心方法与 ArrayList 的区别

    5.2.1 线程安全机制

    Vector 的核心方法(add、get、remove 等)都添加了 synchronized 修饰,例如:

    // Vector 的 add 方法(线程安全)
    public synchronized boolean add(E e) {
    modCount++;
    ensureCapacityHelper(elementCount + 1);
    elementData[elementCount++] = e;
    return true;
    }

    // Vector 的 get 方法(线程安全)
    public synchronized E get(int index) {
    if (index >= elementCount) {
    throw new ArrayIndexOutOfBoundsException(index);
    }
    return elementData(index);
    }

    而 ArrayList 的方法没有 synchronized 修饰,是非线程安全的,多线程环境下并发修改可能导致 ConcurrentModificationException。

    5.2.2 扩容机制

    Vector 的扩容机制与 ArrayList 不同,主要体现在扩容增量上:

    private void grow(int minCapacity) {
    // 当前数组长度
    int oldCapacity = elementData.length;
    // 新容量 = 旧容量 + (扩容增量 > 0 ? 扩容增量 : 旧容量)
    int newCapacity = oldCapacity + ((capacityIncrement > 0) ? capacityIncrement : oldCapacity);
    // 若新容量仍小于所需容量,直接使用所需容量
    if (newCapacity minCapacity < 0) {
    newCapacity = minCapacity;
    }
    // 检查是否超过最大容量限制
    if (newCapacity MAX_ARRAY_SIZE > 0) {
    newCapacity = hugeCapacity(minCapacity);
    }
    // 复制原数组元素到新数组
    elementData = Arrays.copyOf(elementData, newCapacity);
    }

    • ArrayList 每次扩容为原容量的 1.5 倍
    • Vector 若指定了扩容增量(capacityIncrement),则每次扩容增加 capacityIncrement;若未指定(默认 0),则每次扩容为原容量的 2 倍
    5.2.3 构造函数

    Vector 提供了三个构造函数,支持指定初始容量和扩容增量:

    // 无参构造:默认初始容量 10,扩容增量 0
    public Vector() {
    this(DEFAULT_CAPACITY, 0);
    }

    // 指定初始容量,扩容增量 0
    public Vector(int initialCapacity) {
    this(initialCapacity, 0);
    }

    // 指定初始容量和扩容增量
    public Vector(int initialCapacity, int capacityIncrement) {
    super();
    if (initialCapacity < 0) {
    throw new IllegalArgumentException("Illegal Capacity: " + initialCapacity);
    }
    this.elementData = new Object[initialCapacity];
    this.capacityIncrement = capacityIncrement;
    }

    5.3 Vector 的使用场景与注意事项

    5.3.1 适用场景
    • 多线程环境下需要线程安全的 List 操作场景
    • 不需要频繁扩容,且能明确扩容增量的场景
    5.3.2 注意事项

    ⚠️ 注意 1:Vector 的线程安全是通过 synchronized 修饰方法实现的,属于粗粒度锁,并发性能较低。若需要更高的并发性能,推荐使用 Collections.synchronizedList(new ArrayList<>()) 或 CopyOnWriteArrayList(JUC 提供的线程安全 List)。

    // 方案 1:Collections.synchronizedList(线程安全,并发性能优于 Vector)
    List<String> syncList = Collections.synchronizedList(new ArrayList<>());

    // 方案 2:CopyOnWriteArrayList(读写分离,适合读多写少场景)
    List<String> cowList = new CopyOnWriteArrayList<>();

    ⚠️ 注意 2:Vector 与 ArrayList 相比,除了线程安全外,其他性能(如索引访问、扩容效率)基本一致,但由于 synchronized 的开销,单线程环境下不推荐使用 Vector,优先选择 ArrayList。

    ⚠️ 注意 3:Vector 的扩容增量若设置不当,可能导致内存浪费。例如,未指定扩容增量时,每次扩容为 2 倍,若元素数量增长缓慢,会产生大量空闲内存。

    六、List 常见问题与解决方案

    6.1 并发修改异常(ConcurrentModificationException)

    6.1.1 问题现象

    多线程环境下修改 List(如一边遍历一边添加/删除元素),或单线程环境下使用迭代器遍历 List 时修改 List,会抛出 ConcurrentModificationException。

    // 单线程环境下的并发修改异常示例
    List<String> list = new ArrayList<>();
    list.add("A");
    list.add("B");

    // 使用迭代器遍历
    Iterator<String> iterator = list.iterator();
    while (iterator.hasNext()) {
    String element = iterator.next();
    if (element.equals("A")) {
    list.remove(element); // 直接修改 List,触发异常
    }
    }

    6.1.2 问题原因

    List 的迭代器(Iterator)是快速失败(fail-fast)的,迭代器创建时会记录 List 的修改次数(modCount)。遍历过程中,若 List 的修改次数(modCount)与迭代器记录的修改次数(expectedModCount)不一致,会立即抛出异常,防止迭代器继续遍历不一致的数据。

    6.1.3 解决方案

    💡 方案 1:使用迭代器的 remove() 方法修改 List

    Iterator<String> iterator = list.iterator();
    while (iterator.hasNext()) {
    String element = iterator.next();
    if (element.equals("A")) {
    iterator.remove(); // 迭代器内部会同步 modCount 和 expectedModCount
    }
    }

    💡 方案 2:使用增强 for 循环遍历(底层基于迭代器,但不支持在遍历中修改,需结合方案 1)

    💡 方案 3:多线程环境下使用线程安全的 List(如 CopyOnWriteArrayList)

    List<String> list = new CopyOnWriteArrayList<>();
    list.add("A");
    list.add("B");

    // 多线程遍历并修改
    new Thread(() -> {
    for (String element : list) {
    System.out.println(element);
    }
    }).start();

    new Thread(() -> {
    list.remove("A");
    }).start();

    6.2 List 遍历方式效率对比

    6.2.1 遍历方式分类

    List 常见的遍历方式有 4 种:普通 for 循环、增强 for 循环、迭代器遍历、forEach 方法(Java 8+)。

    6.2.2 效率对比测试

    // 测试代码:遍历 100 万条数据的 ArrayList 和 LinkedList
    public class ListTraversalTest {
    public static void main(String[] args) {
    // 初始化 ArrayList(100 万条数据)
    List<Integer> arrayList = new ArrayList<>(1000000);
    for (int i = 0; i < 1000000; i++) {
    arrayList.add(i);
    }

    // 初始化 LinkedList(100 万条数据)
    List<Integer> linkedList = new LinkedList<>();
    for (int i = 0; i < 1000000; i++) {
    linkedList.add(i);
    }

    // 测试 ArrayList 遍历效率
    System.out.println("ArrayList 遍历效率测试:");
    testTraversal(arrayList);

    // 测试 LinkedList 遍历效率
    System.out.println("\\nLinkedList 遍历效率测试:");
    testTraversal(linkedList);
    }

    private static void testTraversal(List<Integer> list) {
    long start, end;

    // 1. 普通 for 循环
    start = System.currentTimeMillis();
    for (int i = 0; i < list.size(); i++) {
    list.get(i);
    }
    end = System.currentTimeMillis();
    System.out.println("普通 for 循环:" + (end start) + "ms");

    // 2. 增强 for 循环
    start = System.currentTimeMillis();
    for (int num : list) {
    // 业务逻辑
    }
    end = System.currentTimeMillis();
    System.out.println("增强 for 循环:" + (end start) + "ms");

    // 3. 迭代器遍历
    start = System.currentTimeMillis();
    Iterator<Integer> iterator = list.iterator();
    while (iterator.hasNext()) {
    iterator.next();
    }
    end = System.currentTimeMillis();
    System.out.println("迭代器遍历:" + (end start) + "ms");

    // 4. forEach 方法(Java 8+)
    start = System.currentTimeMillis();
    list.forEach(num -> {
    // 业务逻辑
    });
    end = System.currentTimeMillis();
    System.out.println("forEach 方法:" + (end start) + "ms");
    }
    }

    6.2.3 测试结果与结论

    ✅ 测试结果(参考值):

    遍历方式ArrayList(100万条)LinkedList(100万条)
    普通 for 循环 1ms 8923ms(极差)
    增强 for 循环 3ms 4ms
    迭代器遍历 2ms 3ms
    forEach 方法 4ms 5ms

    ✅ 结论:

    • ArrayList 优先使用普通 for 循环(索引访问效率最高),其次是增强 for 循环和迭代器
    • LinkedList 严禁使用普通 for 循环(频繁 get(i) 导致 O(n²) 时间复杂度),优先使用增强 for 循环、迭代器或 forEach 方法
    • Java 8+ 的 forEach 方法(基于函数式接口)语法简洁,效率与增强 for 循环接近,推荐使用

    6.3 List 内存优化技巧

    6.3.1 初始化时指定初始容量

    无论是 ArrayList 还是 Vector,初始化时指定合理的初始容量,都能减少扩容次数,降低内存拷贝开销。

    6.3.2 及时清理无用元素,帮助 GC

    对于存储大量对象的 List,不再使用的元素应及时移除(remove() 方法),避免对象引用无法释放,导致内存泄漏。

    6.3.3 使用 trimToSize() 释放空闲内存

    ArrayList 和 Vector 都提供了 trimToSize() 方法,可将数组容量收缩为实际元素个数,释放未使用的空闲内存,适用于大量数据处理后不再添加元素的场景。

    6.3.4 避免使用 LinkedList 存储大量数据

    LinkedList 每个节点额外存储 prev 和 next 引用,内存占用比 ArrayList 高,对于大量数据存储,优先选择 ArrayList。

    七、实战案例:基于 List 的学生成绩管理系统

    7.1 需求分析

    设计一个学生成绩管理系统,支持以下功能:

  • 添加学生成绩(学号、姓名、科目、成绩)
  • 根据学号查询学生所有成绩
  • 根据科目查询该科目所有学生成绩
  • 修改学生某科目的成绩
  • 删除学生某科目的成绩
  • 统计某科目成绩的平均分、最高分、最低分
  • 7.2 设计思路

    • 定义 StudentScore 类封装学生成绩信息
    • 使用 ArrayList 存储所有成绩数据(频繁查询和修改,适合索引访问)
    • 提供工具类实现查询、修改、删除、统计等功能

    7.3 代码实现

    7.3.1 学生成绩实体类(StudentScore.java)

    /**
    * 学生成绩实体类
    */

    public class StudentScore {
    private String studentId; // 学号
    private String studentName; // 姓名
    private String subject; // 科目
    private int score; // 成绩

    // 构造函数
    public StudentScore(String studentId, String studentName, String subject, int score) {
    this.studentId = studentId;
    this.studentName = studentName;
    this.subject = subject;
    // 成绩合法性校验
    if (score < 0 || score > 100) {
    throw new IllegalArgumentException("成绩必须在 0-100 之间");
    }
    this.score = score;
    }

    // getter 和 setter 方法
    public String getStudentId() {
    return studentId;
    }

    public void setStudentId(String studentId) {
    this.studentId = studentId;
    }

    public String getStudentName() {
    return studentName;
    }

    public void setStudentName(String studentName) {
    this.studentName = studentName;
    }

    public String getSubject() {
    return subject;
    }

    public void setSubject(String subject) {
    this.subject = subject;
    }

    public int getScore() {
    return score;
    }

    public void setScore(int score) {
    if (score < 0 || score > 100) {
    throw new IllegalArgumentException("成绩必须在 0-100 之间");
    }
    this.score = score;
    }

    // 重写 toString 方法,方便打印
    @Override
    public String toString() {
    return "StudentScore{" +
    "学号='" + studentId + '\\'' +
    ", 姓名='" + studentName + '\\'' +
    ", 科目='" + subject + '\\'' +
    ", 成绩=" + score +
    '}';
    }
    }

    7.3.2 成绩管理工具类(ScoreManager.java)

    import java.util.ArrayList;
    import java.util.List;
    import java.util.stream.Collectors;

    /**
    * 成绩管理工具类
    */

    public class ScoreManager {
    // 存储所有学生成绩(使用 ArrayList,支持高效查询和修改)
    private List<StudentScore> scoreList = new ArrayList<>();

    /**
    * 添加学生成绩
    * @param score 学生成绩对象
    */

    public void addScore(StudentScore score) {
    if (score == null) {
    throw new IllegalArgumentException("成绩对象不能为空");
    }
    scoreList.add(score);
    System.out.println("添加成功:" + score);
    }

    /**
    * 根据学号查询学生所有成绩
    * @param studentId 学号
    * @return 该学生的所有成绩列表
    */

    public List<StudentScore> queryScoresByStudentId(String studentId) {
    if (studentId == null || studentId.trim().isEmpty()) {
    throw new IllegalArgumentException("学号不能为空");
    }
    // 使用 Stream 过滤,简洁高效
    return scoreList.stream()
    .filter(score -> studentId.equals(score.getStudentId()))
    .collect(Collectors.toList());
    }

    /**
    * 根据科目查询所有学生成绩
    * @param subject 科目
    * @return 该科目的所有成绩列表
    */

    public List<StudentScore> queryScoresBySubject(String subject) {
    if (subject == null || subject.trim().isEmpty()) {
    throw new IllegalArgumentException("科目不能为空");
    }
    return scoreList.stream()
    .filter(score -> subject.equals(score.getSubject()))
    .collect(Collectors.toList());
    }

    /**
    * 修改学生某科目的成绩
    * @param studentId 学号
    * @param subject 科目
    * @param newScore 新成绩
    * @return 修改是否成功
    */

    public boolean updateScore(String studentId, String subject, int newScore) {
    if (studentId == null || studentId.trim().isEmpty()) {
    throw new IllegalArgumentException("学号不能为空");
    }
    if (subject == null || subject.trim().isEmpty()) {
    throw new IllegalArgumentException("科目不能为空");
    }
    if (newScore < 0 || newScore > 100) {
    throw new IllegalArgumentException("成绩必须在 0-100 之间");
    }

    // 遍历查找目标成绩,修改分数
    for (StudentScore score : scoreList) {
    if (studentId.equals(score.getStudentId()) && subject.equals(score.getSubject())) {
    score.setScore(newScore);
    System.out.println("修改成功:学号=" + studentId + ",科目=" + subject + ",新成绩=" + newScore);
    return true;
    }
    }
    System.out.println("修改失败:未找到学号=" + studentId + ",科目=" + subject + "的成绩");
    return false;
    }

    /**
    * 删除学生某科目的成绩
    * @param studentId 学号
    * @param subject 科目
    * @return 删除是否成功
    */

    public boolean deleteScore(String studentId, String subject) {
    if (studentId == null || studentId.trim().isEmpty()) {
    throw new IllegalArgumentException("学号不能为空");
    }
    if (subject == null || subject.trim().isEmpty()) {
    throw new IllegalArgumentException("科目不能为空");
    }

    // 使用迭代器删除,避免并发修改异常
    for (int i = 0; i < scoreList.size(); i++) {
    StudentScore score = scoreList.get(i);
    if (studentId.equals(score.getStudentId()) && subject.equals(score.getSubject())) {
    scoreList.remove(i);
    System.out.println("删除成功:学号=" + studentId + ",科目=" + subject);
    return true;
    }
    }
    System.out.println("删除失败:未找到学号=" + studentId + ",科目=" + subject + "的成绩");
    return false;
    }

    /**
    * 统计某科目成绩的平均分、最高分、最低分
    * @param subject 科目
    * @return 统计结果数组 [平均分, 最高分, 最低分],若无数据返回 null
    */

    public double[] statSubjectScores(String subject) {
    if (subject == null || subject.trim().isEmpty()) {
    throw new IllegalArgumentException("科目不能为空");
    }

    // 获取该科目的所有成绩
    List<StudentScore> subjectScores = queryScoresBySubject(subject);
    if (subjectScores.isEmpty()) {
    System.out.println("统计失败:科目=" + subject + "无成绩数据");
    return null;
    }

    // 计算平均分、最高分、最低分
    int total = 0;
    int max = Integer.MIN_VALUE;
    int min = Integer.MAX_VALUE;
    for (StudentScore score : subjectScores) {
    int s = score.getScore();
    total += s;
    if (s > max) {
    max = s;
    }
    if (s < min) {
    min = s;
    }
    }

    double average = (double) total / subjectScores.size();
    double[] result = new double[3];
    result[0] = average;
    result[1] = max;
    result[2] = min;

    System.out.println("科目=" + subject + " 统计结果:");
    System.out.printf("平均分:%.2f,最高分:%d,最低分:%d%n", average, max, min);
    return result;
    }

    /**
    * 获取所有成绩
    * @return 所有成绩列表
    */

    public List<StudentScore> getAllScores() {
    return new ArrayList<>(scoreList); // 返回副本,避免外部修改
    }
    }

    7.3.3 测试类(ScoreManagerTest.java)

    /**
    * 成绩管理系统测试类
    */

    public class ScoreManagerTest {
    public static void main(String[] args) {
    ScoreManager scoreManager = new ScoreManager();

    // 1. 添加学生成绩
    System.out.println("=== 1. 添加学生成绩 ===");
    scoreManager.addScore(new StudentScore("001", "张三", "Java", 90));
    scoreManager.addScore(new StudentScore("001", "张三", "MySQL", 85));
    scoreManager.addScore(new StudentScore("002", "李四", "Java", 95));
    scoreManager.addScore(new StudentScore("002", "李四", "MySQL", 88));
    scoreManager.addScore(new StudentScore("003", "王五", "Java", 80));
    System.out.println();

    // 2. 根据学号查询成绩
    System.out.println("=== 2. 根据学号查询成绩(学号=001) ===");
    List<StudentScore> zhangSanScores = scoreManager.queryScoresByStudentId("001");
    zhangSanScores.forEach(System.out::println);
    System.out.println();

    // 3. 根据科目查询成绩(Java)
    System.out.println("=== 3. 根据科目查询成绩(科目=Java) ===");
    List<StudentScore> javaScores = scoreManager.queryScoresBySubject("Java");
    javaScores.forEach(System.out::println);
    System.out.println();

    // 4. 修改成绩(学号=001,科目=Java,新成绩=92)
    System.out.println("=== 4. 修改成绩 ===");
    scoreManager.updateScore("001", "Java", 92);
    System.out.println();

    // 5. 删除成绩(学号=003,科目=Java)
    System.out.println("=== 5. 删除成绩 ===");
    scoreManager.deleteScore("003", "Java");
    System.out.println();

    // 6. 统计科目成绩(Java)
    System.out.println("=== 6. 统计科目成绩(科目=Java) ===");
    scoreManager.statSubjectScores("Java");
    System.out.println();

    // 7. 查看所有成绩
    System.out.println("=== 7. 查看所有成绩 ===");
    scoreManager.getAllScores().forEach(System.out::println);
    }
    }

    7.4 案例总结

    ✅ 本案例基于 ArrayList 实现了学生成绩管理系统,充分利用了 ArrayList 索引访问高效、修改便捷的优势,同时解决了 List 使用过程中的常见问题:

  • 输入合法性校验:避免无效数据添加到 List 中
  • 并发修改异常:使用普通 for 循环遍历删除元素,避免迭代器遍历期间修改 List
  • 数据安全性:getAllScores() 方法返回 List 副本,防止外部直接修改内部数据
  • 查询效率:使用 Stream API 简化查询逻辑,同时保证效率
  • ✅ 若系统需要支持频繁添加/删除成绩记录,可将 ArrayList 替换为 LinkedList,但需注意遍历方式的选择(避免普通 for 循环);若系统需要在多线程环境下运行,可将 ArrayList 替换为 CopyOnWriteArrayList,确保线程安全。

    八、本章小结

    本章深入解析了 Java 集合框架中三大 List 实现类(ArrayList、LinkedList、Vector)的底层数据结构、核心方法源码、性能特点及适用场景,通过实战案例展示了 List 的实际应用,同时总结了 List 使用过程中的常见问题与优化技巧。

    核心要点回顾:

  • ArrayList 基于动态数组,索引访问高效,适合频繁查询和末尾增删场景
  • LinkedList 基于双向链表,增删元素高效(尤其是头尾操作),适合频繁修改场景
  • Vector 是线程安全的动态数组,并发性能较低,单线程环境不推荐使用
  • 选择 List 实现类的核心原则:根据操作类型(查询/增删)和线程安全需求决定
  • 性能优化关键:初始化指定容量、选择合适的遍历方式、避免并发修改异常
  • 通过本章学习,读者应能根据实际业务场景灵活选择 List 实现类,并进行针对性优化,提升 Java 程序的性能和稳定性。下一章将深入学习 Java 集合框架中的 Set 接口及其实现类。

    赞(0)
    未经允许不得转载:171主机测评 » Java 集合框架进阶——List 实现类深度解析与实战优化
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址