跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客我的书AI学习GitHub 精选镜像AI 生图工具UI配色美学关于
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
Javajava算法

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

Java 集合框架 List 实现类深度解析与实战优化。文章详细阐述了 ArrayList 动态数组扩容机制、索引访问优势,LinkedList 双向链表增删特性,以及 Vector 线程安全实现。对比遍历效率、内存占用及并发场景表现,提供选择策略与优化技巧,如初始化容量设定、避免并发修改异常。结合学生成绩管理系统实战案例,展示 List 实际应用与注意事项。

蓝绿部署发布于 2026/2/20更新于 2026/10/7102 浏览
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);
}

💡 扩容机制关键点:

  1. 初始容量为 10,每次扩容为原容量的 1.5 倍(通过位运算 oldCapacity >> 1 实现,效率高于除法)
  2. 扩容时会创建新数组,并将原数组元素复制到新数组,这个过程会消耗一定的时间和内存,因此建议在创建 ArrayList 时,根据预期元素数量指定初始容量,减少扩容次数
  3. 最大容量限制为 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++;
}

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

  1. 先通过 node() 方法找到索引位置的节点,该方法通过二分思想优化遍历效率,时间复杂度为 O(n/2) = O(n)
  2. 再通过 linkBefore() 方法插入新节点,仅需修改前后节点的引用,时间复杂度为 O(1)
  3. 整体时间复杂度为 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、RandomAccessList、Deque、Queue
4.4 LinkedList 实用技巧

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

// 作为双端队列使用
Deque<String> deque = new LinkedList<>();
deqe.addFirst("A"); // 头部添加
deqe.addLast("B"); // 尾部添加
String first = deque.getFirst(); // 获取头部元素
String last = deque.getLast(); // 获取尾部元素
deqe.removeFirst(); // 删除头部元素
deqe.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 循环1ms8923ms(极差)
增强 for 循环3ms4ms
迭代器遍历2ms3ms
forEach 方法4ms5ms

✅ 结论:

  • 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 需求分析

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

  1. 添加学生成绩(学号、姓名、科目、成绩)
  2. 根据学号查询学生所有成绩
  3. 根据科目查询该科目所有学生成绩
  4. 修改学生某科目的成绩
  5. 删除学生某科目的成绩
  6. 统计某科目成绩的平均分、最高分、最低分
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 使用过程中的常见问题:

  1. 输入合法性校验:避免无效数据添加到 List 中
  2. 并发修改异常:使用普通 for 循环遍历删除元素,避免迭代器遍历期间修改 List
  3. 数据安全性:getAllScores() 方法返回 List 副本,防止外部直接修改内部数据
  4. 查询效率:使用 Stream API 简化查询逻辑,同时保证效率

✅ 若系统需要支持频繁添加/删除成绩记录,可将 ArrayList 替换为 LinkedList,但需注意遍历方式的选择(避免普通 for 循环);若系统需要在多线程环境下运行,可将 ArrayList 替换为 CopyOnWriteArrayList,确保线程安全。

八、本章小结

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

核心要点回顾:

  1. ArrayList 基于动态数组,索引访问高效,适合频繁查询和末尾增删场景
  2. LinkedList 基于双向链表,增删元素高效(尤其是头尾操作),适合频繁修改场景
  3. Vector 是线程安全的动态数组,并发性能较低,单线程环境不推荐使用
  4. 选择 List 实现类的核心原则:根据操作类型(查询/增删)和线程安全需求决定
  5. 性能优化关键:初始化指定容量、选择合适的遍历方式、避免并发修改异常

通过本章学习,读者应能根据实际业务场景灵活选择 List 实现类,并进行针对性优化,提升 Java 程序的性能和稳定性。下一章将深入学习 Java 集合框架中的 Set 接口及其实现类。

目录

  1. Java 集合框架进阶——List 实现类深度解析与实战优化
  2. 一、章节学习目标与重点
  3. 1.1 学习目标
  4. 1.2 学习重点
  5. 二、List 接口核心特性回顾
  6. 三、ArrayList 深度解析:动态数组的设计与优化
  7. 3.1 底层数据结构
  8. 3.2 核心方法源码解析
  9. 3.2.1 add() 方法:元素添加与扩容机制
  10. (1)末尾添加元素:add(E e)
  11. (2)指定索引添加元素:add(int index, E element)
  12. 3.2.2 get() 方法:索引访问的高效实现
  13. 3.2.3 remove() 方法:元素删除与数组收缩
  14. 3.3 ArrayList 性能分析与使用场景
  15. 3.3.1 性能特点
  16. 3.3.2 适用场景
  17. 3.3.3 性能优化技巧
  18. 四、LinkedList 深度解析:双向链表的设计与实现
  19. 4.1 底层数据结构
  20. 4.2 核心方法源码解析
  21. 4.2.1 add() 方法:节点添加机制
  22. (1)末尾添加元素:add(E e)
  23. (2)指定索引添加元素:add(int index, E element)
  24. 4.2.2 get() 方法:索引访问的实现
  25. 4.2.3 remove() 方法:节点删除机制
  26. 4.3 LinkedList 性能分析与使用场景
  27. 4.3.1 性能特点
  28. 4.3.2 适用场景
  29. 4.3.3 LinkedList 与 ArrayList 核心区别对比
  30. 4.4 LinkedList 实用技巧
  31. 五、Vector 解析:线程安全的动态数组
  32. 5.1 底层数据结构与核心特性
  33. 5.2 核心方法与 ArrayList 的区别
  34. 5.2.1 线程安全机制
  35. 5.2.2 扩容机制
  36. 5.2.3 构造函数
  37. 5.3 Vector 的使用场景与注意事项
  38. 5.3.1 适用场景
  39. 5.3.2 注意事项
  40. 六、List 常见问题与解决方案
  41. 6.1 并发修改异常(ConcurrentModificationException)
  42. 6.1.1 问题现象
  43. 6.1.2 问题原因
  44. 6.1.3 解决方案
  45. 6.2 List 遍历方式效率对比
  46. 6.2.1 遍历方式分类
  47. 6.2.2 效率对比测试
  48. 6.2.3 测试结果与结论
  49. 6.3 List 内存优化技巧
  50. 6.3.1 初始化时指定初始容量
  51. 6.3.2 及时清理无用元素,帮助 GC
  52. 6.3.3 使用 trimToSize() 释放空闲内存
  53. 6.3.4 避免使用 LinkedList 存储大量数据
  54. 七、实战案例:基于 List 的学生成绩管理系统
  55. 7.1 需求分析
  56. 7.2 设计思路
  57. 7.3 代码实现
  58. 7.3.1 学生成绩实体类(StudentScore.java)
  59. 7.3.2 成绩管理工具类(ScoreManager.java)
  60. 7.3.3 测试类(ScoreManagerTest.java)
  61. 7.4 案例总结
  62. 八、本章小结

更多推荐文章

查看全部
  • Ubuntu 22.04 安装与配置 ROS2 Humble 详解
  • 三款 GitHub 高星开源音乐播放与管理工具推荐
  • 二分查找实战:旋转排序数组最小值与缺失数字求解
  • Quest 一体机通过 SideQuest 安装本地游戏与 APK 教程
  • VSCode 中 GitHub Copilot 的多模型架构与 Agent 调度机制
  • 基于 PyTorch 从零构建与训练大型语言模型入门指南(上)
  • ZeroClaw:基于 Rust 的低内存 AI Agent 框架解析与实战
  • OpenCode 安装 oh-my-opencode 插件教程:AI 辅助自动配置
  • AI Agent Skills 资源合集,支持 Cursor、Claude Code 及 Copilot 快速部署
  • Java JDK 21 安装与环境配置教程(Windows/macOS 通用)
  • 5 个 nano banana 提示词资源网站及生成技巧
  • cxx-qt 底层机制:实现 C++ 与 Rust 无缝通信及开发效率提升
  • 无人机通信协议解析:MAVLink、PPM 与 SBUS 原理及适用场景
  • OpenCore 安装指南:在 PC 上运行 macOS 的完整教程
  • 大模型幻觉问题治理:技术体系与工程实践
  • 鸿蒙金融理财全栈项目:基础架构、数据安全与用户体验
  • GitHub Copilot 性能优化实战:提升 AI 代码建议响应速度
  • 企业为何弃用 MinIO 及开源替代方案分析
  • MySQL MCP 服务本地部署与 Cherry Studio 应用实践
  • OpenClaw Skills 安装与实战:构建 AI 技能工具箱

相关免费在线工具

  • Keycode 信息

    查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online

  • Escape 与 Native 编解码

    JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online

  • JavaScript / HTML 格式化

    使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online

  • JavaScript 压缩与混淆

    Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online

  • 加密/解密文本

    使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online

  • Gemini 图片去水印

    基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online