Java List集合深度解析:从基础用法到实战技巧

Java List集合深度解析:从基础用法到实战技巧全攻略

一、List 集合核心概念与特性


1.1 List 接口定义与本质

List 是 Java 集合框架中Collection接口的子接口,代表有序的元素序列。其核心特性包括:

  1. 有序性:元素按插入顺序存储,索引从 0 开始。例如,依次向 List 中添加元素 A、B、C,那么通过索引获取时,顺序也为 A、B、C。

  2. 可重复性 :允许存储相同元素(equals()方法返回true)。比如,一个 List 中可以多次添加字符串 "hello"。

  3. 动态扩容 :实现类自动管理容量(如ArrayList)。当添加元素超过当前容量时,会自动扩大内部数组的大小,以容纳更多元素。

  4. 泛型支持 :通过List<E>保证类型安全。在定义 List 时指定元素类型,如List<Integer>,这样在编译期就能检查类型错误,避免运行时的类型转换异常。

1.2 与数组的本质区别

特性 List 数组
长度 动态可变(通过扩容机制) 固定(初始化后不可变)
元素类型 支持泛型(编译期类型检查) 基本类型 / 引用类型数组
操作方法 丰富的 API(增删改查迭代器) 仅通过索引访问
内存分配 自动管理内存(内部数组扩容) 需手动分配连续内存空间

从长度上看,数组在创建时就确定了大小,后续无法改变;而 List 的大小可以根据元素的添加和删除动态变化。在元素类型方面,数组可以是基本类型或引用类型,而 List 只能存储引用类型,通过泛型来指定具体的对象类型,增强了类型安全性。操作方法上,List 提供了如addremovecontainsiterator等丰富的方法来操作元素,而数组主要通过索引进行访问和赋值。内存分配上,数组需要手动指定大小并分配连续的内存空间,而 List 内部自动管理内存,通过动态扩容来适应元素数量的变化 。

二、核心实现类对比与选型指南


2.1 四大主流实现类详解

2.1.1 ArrayList:数组驱动的通用列表
  1. 数据结构 :基于动态数组实现(Object[] elementData),元素在内存中连续存储,实现了RandomAccess接口,支持快速随机访问。

  2. 性能特点

    • 随机访问( get(index) :时间复杂度为 O (1),因为可以直接通过数组下标进行寻址,就像在一排编好号的柜子中,直接根据编号就能快速找到对应的柜子。

    • 中间插入 / 删除( add(index, e) :时间复杂度为 O (n),因为在中间位置插入或删除元素时,需要移动后续的元素。例如,在一个队伍中间插入一个人,后面的人都需要往后移动一个位置。

    • 尾部操作( add(e) :均摊时间复杂度为 O (1) ,通常情况下,在尾部添加元素很快,只有当内部数组容量不足,触发扩容时,才需要进行数组复制,将原数组元素复制到新的更大的数组中 。

  3. 适用场景

    • 默认首选 :在大多数情况下,如果没有特殊的性能要求,ArrayList是一个很好的选择,因为它综合性能较好,使用简单。

    • 频繁读取、尾部追加场景 :比如在数据库结果集封装时,从数据库中查询出一批数据,需要将这些数据存储在一个集合中,然后进行后续的处理。由于查询结果通常是顺序获取的,并且后续可能只是对这些数据进行读取操作,所以使用ArrayList非常合适 。

2.1.2 LinkedList:双向链表的高效变更
  1. 数据结构 :双向链表(每个节点包含prevnext指针),每个节点不仅知道下一个节点的位置,还知道上一个节点的位置,这使得双向遍历成为可能。

  2. 性能特点

    • 随机访问:时间复杂度为 O (n),因为需要从头开始遍历链表,直到找到目标索引的节点。这就好比在一条长街上找一个门牌号,必须从街头一个一个门牌号数过去。

    • 中间插入 / 删除:时间复杂度为 O (1),在已知插入或删除位置的情况下,只需修改相关节点的指针引用即可,不需要移动大量元素。例如,在一个环形的队伍中,让两个人交换位置,只需要调整他们与相邻人的牵手关系。

    • 内存占用 :高于ArrayList,因为每个节点除了存储数据本身,还需要额外存储两个指针,分别指向前一个节点和后一个节点。

  3. 适用场景

    • 频繁中间插入 / 删除场景 :比如实现队列的offer/poll操作,队列是一种先进先出的数据结构,LinkedList可以高效地在头部和尾部进行插入和删除操作,非常适合实现队列。再比如实现撤销操作历史记录,每一次操作都可以作为一个节点添加到链表中,当需要撤销时,直接从链表中删除最后一个节点即可。
2.1.3 Vector:线程安全的历史选择
  1. 数据结构 :基于数组实现(与ArrayList类似),内部也是通过一个数组来存储元素。

  2. 核心特性

    • 方法同步 :所有操作通过synchronized修饰,这使得Vector在多线程环境下是线程安全的,但同时也带来了性能损耗,大约会有 20%-30% 的性能下降,因为加锁会导致线程之间的竞争和等待。

    • 扩容策略 :默认扩容为原容量的 2 倍,而ArrayList默认扩容为原容量的 1.5 倍。例如,当Vector的当前容量为 10,需要扩容时,新的容量会变为 20。

  3. 适用场景

    • 遗留系统兼容 :在一些早期的 Java 系统中,可能会使用Vector来保证线程安全。但在新的开发场景中,由于其性能问题,通常建议使用Collections.synchronizedList(new ArrayList<>())来代替,这种方式可以在需要线程安全的情况下,通过对ArrayList进行包装,实现线程安全,同时又能利用ArrayList的性能优势。
2.1.4 CopyOnWriteArrayList:高并发下的读写分离
  1. 数据结构:写时复制(每次写操作创建新数组副本),当有写操作发生时,会先复制一份原数组,然后在新的数组上进行修改,最后将原数组的引用指向新数组。

  2. 并发特性

    • 读操作 :不加锁,直接访问原数组,因此读操作非常快,适用于 99% 读场景,比如在一个高并发的系统中,大量的线程只是读取配置信息,而很少有线程去修改配置,使用CopyOnWriteArrayList可以大大提高读取性能。

    • 写操作:加锁并复制数组,这使得写操作的开销较大,所以适用于多读少写场景。在写操作时,先获取锁,然后复制数组,在新数组上进行修改,最后释放锁。

    • 迭代器:基于快照机制,迭代器在创建时会获取当前数组的一个快照,在迭代过程中不会反映后续的写操作,这保证了迭代过程的一致性,但也意味着迭代器可能读取到的不是最新的数据。

  3. 适用场景

    • 高并发读取 :如日志监控系统,大量的线程需要读取日志信息进行分析,而写操作相对较少,使用CopyOnWriteArrayList可以提高读取性能,保证系统的高效运行。再比如配置信息缓存,配置信息通常很少修改,但会被多个线程频繁读取,使用CopyOnWriteArrayList可以满足这种高并发读取的需求。

2.2 选型决策树

在实际应用中,选择合适的List实现类可以显著提升程序性能。以下是一个简单的选型决策树:

plantuml 复制代码
@startmindmap
* 是否需要线程安全?
** 是
*** 是否读多写少?
**** 是: CopyOnWriteArrayList
**** 否: Collections.synchronizedList(new ArrayList<>()) 或 Vector(不推荐)
** 否
*** 是否频繁随机访问?
**** 是: ArrayList
**** 否
***** 是否频繁中间插入/删除?
****** 是: LinkedList
****** 否: ArrayList
@enduml
  1. 首先判断是否需要线程安全。如果需要,再看是否是读多写少的场景,如果是,选择CopyOnWriteArrayList;如果不是,选择Collections.synchronizedList(new ArrayList<>()) ,尽量避免使用Vector

  2. 如果不需要线程安全,接着判断是否频繁进行随机访问。如果是,选择ArrayList;如果不是,再判断是否频繁进行中间插入 / 删除操作,如果是,选择LinkedList;如果不是,也选择ArrayList

三、基础操作与核心 API 详解


3.1 创建与初始化最佳实践

3.1.1 标准初始化方式
  1. 常规构造器初始化 :使用ArrayListLinkedList的无参构造器,后续通过add方法逐个添加元素。
java 复制代码
List<String> list1 = new ArrayList<>();
list1.add("apple");
list1.add("banana");
  1. Arrays.asList() :将数组转换为List,但返回的List是固定大小的,不支持添加或删除元素操作(会抛出UnsupportedOperationException)。
java 复制代码
String[] array = {"apple", "banana"};
List<String> list2 = Arrays.asList(array);
  1. List.of()(Java 9+) :创建不可变List,简洁高效,不允许null元素,且不支持修改操作。
java 复制代码
List<String> list3 = List.of("apple", "banana");
  1. Stream 初始化 :利用 Java 8 的Stream API 创建List,适合链式操作和复杂数据生成场景。
java 复制代码
List<String> list4 = Stream.of("apple", "banana")
                           .collect(Collectors.toList());
3.1.2 初始化陷阱
  1. List.of () 的不可修改性List.of()创建的列表是不可修改的,尝试调用addremove方法会抛出UnsupportedOperationException异常。这是因为List.of()返回的是一个不可变的列表实现,其设计目的就是为了提供只读的列表视图,确保数据的不可变性,常用于创建常量列表等场景。
java 复制代码
List<String> list = List.of("a", "b", "c");
// 以下操作会抛出UnsupportedOperationException
list.add("d"); 
list.remove("a");
  1. Arrays.asList () 的数组关联Arrays.asList()返回的列表与原数组共享内存,对列表元素的修改会影响原数组,反之亦然。这是因为Arrays.asList()返回的List是基于原数组的一个视图,它并没有创建一个新的独立的数据结构。如果后续需要对列表进行独立的修改操作,应使用new ArrayList<>(Arrays.asList(array))将其包装成一个真正的ArrayList
java 复制代码
String[] array = {"a", "b", "c"};
List<String> list = Arrays.asList(array);
list.set(0, "d");
System.out.println(Arrays.toString(array)); // 输出: [d, b, c]
array[1] = "e";
System.out.println(list); // 输出: [d, e, c]

3.2 增删改查核心操作

3.2.1 添加操作
  1. add(E e) :将元素追加到列表末尾,ArrayList的均摊时间复杂度为 O (1),LinkedList为 O (1)。
java 复制代码
List<Integer> list = new ArrayList<>();
list.add(1);
  1. add(int index, E element) :在指定索引处插入元素,ArrayList时间复杂度为 O (n)(需移动后续元素),LinkedList为 O (1)(修改指针)。
java 复制代码
list.add(1, 2);
  1. addAll(Collection<? extends E> c):将另一个集合的所有元素添加到当前列表末尾,时间复杂度与被添加集合的大小相关。
java 复制代码
List<Integer> anotherList = Arrays.asList(3, 4);
list.addAll(anotherList);
3.2.2 删除操作
  1. remove(Object o) :删除指定元素的第一个匹配项,ArrayList时间复杂度为 O (n)(需查找并移动元素),LinkedList为 O (n)(需查找并修改指针)。
java 复制代码
list.remove(Integer.valueOf(2));
  1. remove(int index) :删除指定索引处的元素,ArrayList时间复杂度为 O (n),LinkedList为 O (n)。
java 复制代码
list.remove(0);
  1. removeAll(Collection<?> c):删除当前列表中包含在指定集合中的所有元素,时间复杂度与两个集合的大小相关。
java 复制代码
List<Integer> removeList = Arrays.asList(3, 4);
list.removeAll(removeList);
3.2.3 修改与查询
  1. set(int index, E element) :替换指定索引处的元素,返回被替换的旧元素,ArrayListLinkedList时间复杂度均为 O (1)(LinkedList需先定位到节点)。
java 复制代码
Integer oldValue = list.set(0, 5);
  1. get(int index) :获取指定索引处的元素,ArrayList时间复杂度为 O (1),LinkedList为 O (n)。
java 复制代码
Integer element = list.get(0);
  1. indexOf(Object o):返回指定元素首次出现的索引,若不存在则返回 -1,时间复杂度为 O (n)。
java 复制代码
int index = list.indexOf(5);
  1. lastIndexOf(Object o):返回指定元素最后一次出现的索引,若不存在则返回 -1,时间复杂度为 O (n)。
java 复制代码
int lastIndex = list.lastIndexOf(5);

3.3 遍历方式对比与性能优化

3.3.1 五种遍历方式对比
遍历方式 实现原理 适用场景 性能特点
普通 for 循环 索引访问 ArrayList 高效,LinkedList 低效 ArrayList: O(n), LinkedList: O(n²)
增强 for 循环 内部使用 Iterator 简单遍历(无需索引) 语法糖,性能等同 Iterator
Iterator 迭代器 指针遍历(支持 fail-fast) 需中途删除元素 推荐 LinkedList 遍历
ListIterator 双向遍历(支持前向 / 后向) 复杂顺序操作 提供previous等方法
Stream 流式处理 函数式操作(过滤 / 映射) 数据转换与聚合 并行流支持多核优化
  1. 普通 for 循环 :通过索引访问元素,适用于ArrayList,因为ArrayList支持快速随机访问,时间复杂度为 O (n)。但对于LinkedList,每次通过索引访问都需要从头开始遍历链表,时间复杂度为 O (n²),效率较低。
java 复制代码
List<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));
for (int i = 0; i < list.size(); i++) {
    Integer num = list.get(i);
    System.out.println(num);
}
  1. 增强 for 循环 :语法简洁,适用于简单遍历,无需关心索引。其底层使用Iterator实现,性能与Iterator遍历基本相同,时间复杂度为 O (n)。
java 复制代码
for (Integer num : list) {
    System.out.println(num);
}
  1. Iterator 迭代器 :通过hasNext()next()方法遍历,支持在遍历过程中安全删除元素(使用remove()方法),适用于需要在遍历过程中删除元素的场景,对于LinkedList尤为适用,时间复杂度为 O (n)。
java 复制代码
Iterator<Integer> iterator = list.iterator();
while (iterator.hasNext()) {
    Integer num = iterator.next();
    if (num == 3) {
        iterator.remove();
    }
    System.out.println(num);
}
  1. ListIterator :继承自Iterator,支持双向遍历(hasPrevious()previous()方法),还可以在遍历过程中修改元素(set()方法),适用于需要双向操作或在遍历中修改元素的场景 。
java 复制代码
ListIterator<Integer> listIterator = list.listIterator();
while (listIterator.hasNext()) {
    Integer num = listIterator.next();
    if (num == 4) {
        listIterator.set(40);
    }
    System.out.println(num);
}
while (listIterator.hasPrevious()) {
    Integer num = listIterator.previous();
    System.out.println(num);
}
  1. Stream 流式处理 :提供函数式编程风格,支持链式操作,如过滤、映射、聚合等。可以使用并行流(parallelStream())充分利用多核 CPU 进行并行处理,提高处理大数据集的效率,时间复杂度根据具体操作而定 。
java 复制代码
list.stream()
   .filter(num -> num > 2)
   .map(num -> num * 2)
   .forEach(System.out::println);
3.3.2 遍历陷阱
  1. 普通 for 循环删除元素 :在普通 for 循环中删除元素会导致索引混乱,因为删除元素后,后续元素会向前移动,索引会发生变化。如果继续按照原索引进行遍历,会跳过一些元素。正确的做法是使用Iteratorremove()方法来删除元素。
java 复制代码
List<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));
// 错误示范,会导致索引混乱
for (int i = 0; i < list.size(); i++) {
    if (list.get(i) == 3) {
        list.remove(i);
    }
}
System.out.println(list); 

// 正确示范,使用Iterator删除元素
Iterator<Integer> iterator = list.iterator();
while (iterator.hasNext()) {
    Integer num = iterator.next();
    if (num == 3) {
        iterator.remove();
    }
}
System.out.println(list);
  1. 并发遍历异常 :在并发环境下,如果多个线程同时对List进行遍历和修改操作,未加锁的情况下可能会触发ConcurrentModificationException异常,这是由于fail - fast机制导致的。fail - fast机制会在集合的结构被意外修改时快速抛出异常,以保证数据的一致性和安全性。为了避免这种异常,可以使用线程安全的List实现(如CopyOnWriteArrayList),或者在遍历和修改时使用同步锁 。
java 复制代码
List<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));
// 线程1遍历
new Thread(() -> {
    for (Integer num : list) {
        System.out.println(num);
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}).start();

// 线程2修改
new Thread(() -> {
    try {
        Thread.sleep(200);
        list.remove(2);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}).start();

上述代码中,线程 1 在遍历List,线程 2 在中途修改List,可能会导致ConcurrentModificationException异常。如果使用CopyOnWriteArrayList,则可以避免这个问题,因为CopyOnWriteArrayList的读操作不加锁,写操作时会复制数组,保证了读操作的一致性 。

java 复制代码
CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>(Arrays.asList(1, 2, 3, 4, 5));
// 线程1遍历
new Thread(() -> {
    for (Integer num : list) {
        System.out.println(num);
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}).start();

// 线程2修改
new Thread(() -> {
    try {
        Thread.sleep(200);
        list.remove(2);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}).start();

四、高级操作与工具类应用


4.1 排序与数据转换

4.1.1 排序策略
  1. 自然排序 :元素实现Comparable接口,调用list.sort(null)Collections.sort(list)进行自然升序排序。例如,String类和Integer类都实现了Comparable接口,可以直接进行自然排序。
java 复制代码
List<Integer> list = new ArrayList<>(Arrays.asList(3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5));
list.sort(null);
System.out.println(list);
  1. 自定义排序 :使用Comparator接口,通过list.sort(Comparator)Collections.sort(list, Comparator)实现。例如,对自定义类Student按年龄排序。
java 复制代码
class Student {
    private String name;
    private int age;

    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public int getAge() {
        return age;
    }
}

List<Student> studentList = new ArrayList<>();
studentList.add(new Student("Alice", 20));
studentList.add(new Student("Bob", 18));
studentList.add(new Student("Charlie", 22));

studentList.sort(Comparator.comparingInt(Student::getAge));
System.out.println(studentList);
  1. 逆序排序 :使用Collections.reverseOrder()获取逆序Comparator,或在自定义Comparator中反转比较逻辑。
java 复制代码
list.sort(Collections.reverseOrder());
System.out.println(list);
4.1.2 数据转换
  1. List 与数组互转

    • List 转数组 :调用toArray()方法,可指定目标数组类型。
java 复制代码
List<String> list = Arrays.asList("apple", "banana", "cherry");
String[] array = list.toArray(new String[0]);
  • 数组转 List :使用Arrays.asList(),但返回的List不可修改大小。
java 复制代码
String[] array = {"apple", "banana", "cherry"};
List<String> list = Arrays.asList(array);
  1. List 与 Set 互转

    • List 转 Set :利用HashSetTreeSet构造器,可去除重复元素。
java 复制代码
List<Integer> list = Arrays.asList(1, 2, 2, 3, 3, 3);
Set<Integer> set = new HashSet<>(list);
  • Set 转 List :通过ArrayList构造器,可保持顺序。
java 复制代码
Set<Integer> set = new HashSet<>(Arrays.asList(1, 2, 3));
List<Integer> list = new ArrayList<>(set);

4.2 子列表与视图操作

  1. subList(int fromIndex, int toIndex) :返回原列表的部分视图(非副本),索引范围为[fromIndex, toIndex),修改子列表会影响原列表,反之亦然。例如,从一个包含 10 个整数的列表中获取第 3 到第 5 个元素(索引从 0 开始)。
java 复制代码
List<Integer> list = new ArrayList<>();
for (int i = 1; i <= 10; i++) {
    list.add(i);
}
List<Integer> subList = list.subList(2, 5);
System.out.println(subList); 
subList.set(0, 100);
System.out.println(list);
  1. 视图操作注意事项

    • 生命周期:子列表视图的生命周期依赖于原列表,原列表销毁时,子列表也不可用。

    • 结构修改:在原列表进行结构修改(如删除元素)可能导致子列表不可预测行为,尽量在子列表上进行修改操作 。

4.3 并发场景处理

  1. 线程安全的 List 实现

    • Vector :所有方法同步,性能较低,已逐渐被弃用。在早期的 Java 系统中,Vector被广泛用于多线程环境,但由于其所有方法都使用synchronized关键字进行同步,导致在高并发场景下,线程竞争激烈,性能下降明显 。

    • Collections.synchronizedList(List list) :包装非线程安全List,手动同步迭代器操作,在遍历List时,需要使用synchronized块来同步对迭代器的访问,以避免ConcurrentModificationException异常 。

    • CopyOnWriteArrayList:写时复制,适合读多写少场景,读操作不加锁,写操作加锁并复制数组 。

  2. 避免并发修改异常

    • 使用迭代器安全删除 :在多线程环境下,使用Iteratorremove()方法删除元素,而不是直接调用Listremove()方法,以避免ConcurrentModificationException异常。

    • 同步控制 :使用Collections.synchronizedList时,使用synchronized块同步遍历和修改操作。

java 复制代码
List<Integer> synchronizedList = Collections.synchronizedList(new ArrayList<>());
synchronized (synchronizedList) {
    Iterator<Integer> iterator = synchronizedList.iterator();
    while (iterator.hasNext()) {
        Integer num = iterator.next();
        if (num % 2 == 0) {
            iterator.remove();
        }
    }
}

五、实际应用场景与代码示例


5.1 数据库结果集封装

在数据库操作中,经常需要将查询结果封装成List集合,以便在 Java 程序中进行后续处理。以查询用户表为例,使用 JDBC 和ArrayList实现结果集封装:

java 复制代码
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;

class User {
    private int id;
    private String name;
    private String email;

    public User(int id, String name, String email) {
        this.id = id;
        this.name = name;
        this.email = email;
    }

    // Getter and Setter方法
    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", email='" + email + '\'' +
                '}';
    }
}

public class DatabaseExample {
    public static void main(String[] args) {
        String url = "jdbc:mysql://localhost:3306/mydb";
        String username = "root";
        String password = "password";

        List<User> userList = new ArrayList<>();

        try (Connection connection = DriverManager.getConnection(url, username, password);
             Statement statement = connection.createStatement();
             ResultSet resultSet = statement.executeQuery("SELECT id, name, email FROM users")) {

            while (resultSet.next()) {
                int id = resultSet.getInt("id");
                String name = resultSet.getString("name");
                String email = resultSet.getString("email");
                User user = new User(id, name, email);
                userList.add(user);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

        userList.forEach(System.out::println);
    }
}

在上述代码中,通过 JDBC 连接数据库,执行 SQL 查询,将每一行结果封装成User对象,并添加到ArrayList中。最后遍历List,打印用户信息。

5.2 Spring Boot 接收批量参数

在 Spring Boot 应用中,接收前端传递的批量参数(如批量删除操作)时,List集合非常实用。通过@RequestParam@RequestBody注解可以方便地接收List参数。

  1. 使用 @RequestParam 接收 GET 请求参数:假设前端传递多个用户 ID 进行批量操作,后端 Controller 代码如下:
java 复制代码
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
public class UserController {

    @GetMapping("/users/delete")
    public String deleteUsers(@RequestParam List<Integer> userIds) {
        // 执行批量删除逻辑
        for (Integer userId : userIds) {
            // 调用Service层删除方法
            System.out.println("Deleting user with ID: " + userId);
        }
        return "Users deleted successfully";
    }
}

在上述代码中,@RequestParam注解用于获取 URL 中的userIds参数,多个参数值会自动封装成List<Integer>

  1. 使用 @RequestBody 接收 POST 请求参数 :当参数以 JSON 格式传递时,使用@RequestBody注解。假设前端传递用户对象列表进行批量保存,后端代码如下:
java 复制代码
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

class UserDTO {
    private int id;
    private String name;
    private String email;

    // Getter and Setter方法
    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    @Override
    public String toString() {
        return "UserDTO{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", email='" + email + '\'' +
                '}';
    }
}

@RestController
public class UserController {

    @PostMapping("/users/save")
    public String saveUsers(@RequestBody List<UserDTO> userDTOList) {
        // 执行批量保存逻辑
        for (UserDTO userDTO : userDTOList) {
            // 调用Service层保存方法
            System.out.println("Saving user: " + userDTO);
        }
        return "Users saved successfully";
    }
}

在上述代码中,@RequestBody注解将请求体中的 JSON 数据自动转换为List<UserDTO>,方便进行批量处理。

5.3 简单缓存实现(FIFO 策略)

使用LinkedListHashMap实现一个简单的 FIFO(先进先出)缓存,当缓存满时,移除最早加入的元素。

java 复制代码
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;

public class FIFOCache<K, V> {
    private final int capacity;
    private final LinkedList<K> queue;
    private final Map<K, V> cache;

    public FIFOCache(int capacity) {
        this.capacity = capacity;
        this.queue = new LinkedList<>();
        this.cache = new HashMap<>();
    }

    public void put(K key, V value) {
        if (cache.containsKey(key)) {
            // 如果键已存在,更新值并调整队列(不需要,因为FIFO不考虑访问顺序)
            cache.put(key, value);
        } else {
            if (queue.size() >= capacity) {
                // 缓存已满,移除最早的元素
                K oldestKey = queue.poll();
                cache.remove(oldestKey);
            }
            // 添加新元素
            queue.addLast(key);
            cache.put(key, value);
        }
    }

    public V get(K key) {
        return cache.get(key);
    }

    public static void main(String[] args) {
        FIFOCache<String, Integer> cache = new FIFOCache<>(3);
        cache.put("A", 1);
        cache.put("B", 2);
        cache.put("C", 3);
        System.out.println(cache.get("A")); 
        cache.put("D", 4);
        System.out.println(cache.get("A")); 
    }
}

在上述代码中,LinkedList用于维护元素的插入顺序,HashMap用于存储键值对。put方法在缓存满时移除最早的元素,get方法用于获取缓存值。

六、常见误区与最佳实践


6.1 六大高频错误

  1. 遍历删除错误 :在使用增强 for 循环遍历List时直接删除元素,会导致ConcurrentModificationException异常,因为增强 for 循环底层使用Iterator,在遍历过程中修改集合结构会触发fail - fast机制。正确的做法是使用Iteratorremove方法进行删除操作,这样可以保证在删除元素时,Iterator内部的状态能够正确更新,避免异常的发生 。

    • ❌ 错误:
java 复制代码
List<String> list = new ArrayList<>(Arrays.asList("apple", "banana", "cherry"));
for (String s : list) {
    if ("banana".equals(s)) {
        list.remove(s);
    }
}
  • ✅ 正确:
java 复制代码
List<String> list = new ArrayList<>(Arrays.asList("apple", "banana", "cherry"));
Iterator<String> it = list.iterator();
while (it.hasNext()) {
    String s = it.next();
    if ("banana".equals(s)) {
        it.remove();
    }
}
  1. 类型比较错误 :使用==比较List中的元素与目标值,对于引用类型,==比较的是对象的内存地址,而不是值本身。应该使用Objects.equals方法来比较对象的值,Objects.equals方法会先判断两个对象是否为null,然后再进行值的比较,避免了空指针异常 。

    • ❌ 错误:
java 复制代码
List<String> list = new ArrayList<>(Arrays.asList("apple", "banana", "cherry"));
if (list.get(0) == "apple") {
    System.out.println("Equal");
}
  • ✅ 正确:
java 复制代码
List<String> list = new ArrayList<>(Arrays.asList("apple", "banana", "cherry"));
if (Objects.equals(list.get(0), "apple")) {
    System.out.println("Equal");
}
  1. 未初始化直接使用 :声明List变量后未初始化就调用add方法,会导致NullPointerException,因为未初始化的List变量值为null,没有指向任何实际的集合对象,调用add方法时,实际上是在一个空引用上调用方法,必然会抛出空指针异常。必须先通过构造函数初始化List,如new ArrayList<>(),为其分配内存空间并创建实际的集合对象,才能进行后续的操作 。

    • ❌ 错误:
java 复制代码
List<String> list;
list.add("a");
  • ✅ 正确:
java 复制代码
List<String> list = new ArrayList<>();
list.add("a");
  1. 忽略线程安全 :在多线程环境下直接使用ArrayList,可能会导致数据不一致或ConcurrentModificationException异常,因为ArrayList不是线程安全的,多个线程同时对其进行读写操作时,可能会出现数据竞争和不一致的情况。应根据场景选择Collections.synchronizedList(new ArrayList<>())CopyOnWriteArrayList,前者通过同步方法来保证线程安全,后者通过写时复制的机制,在读写分离的情况下保证线程安全 。

    • ❌ 错误:多线程环境直接使用ArrayList
java 复制代码
List<Integer> list = new ArrayList<>();
// 线程1
new Thread(() -> {
    for (int i = 0; i < 10; i++) {
        list.add(i);
    }
}).start();
// 线程2
new Thread(() -> {
    for (int i = 10; i < 20; i++) {
        list.add(i);
    }
}).start();
  • ✅ 正确:根据场景选择synchronizedListCopyOnWriteArrayList
java 复制代码
// 使用synchronizedList
List<Integer> synchronizedList = Collections.synchronizedList(new ArrayList<>());
// 线程1
new Thread(() -> {
    synchronized (synchronizedList) {
        for (int i = 0; i < 10; i++) {
            synchronizedList.add(i);
        }
    }
}).start();
// 线程2
new Thread(() -> {
    synchronized (synchronizedList) {
        for (int i = 10; i < 20; i++) {
            synchronizedList.add(i);
        }
    }
}).start();

// 使用CopyOnWriteArrayList
List<Integer> copyOnWriteList = new CopyOnWriteArrayList<>();
// 线程1
new Thread(() -> {
    for (int i = 0; i < 10; i++) {
        copyOnWriteList.add(i);
    }
}).start();
// 线程2
new Thread(() -> {
    for (int i = 10; i < 20; i++) {
        copyOnWriteList.add(i);
    }
}).start();
  1. 误用不可变列表 :对List.of()返回的列表调用add方法,会抛出UnsupportedOperationException异常,因为List.of()创建的是不可变列表,不支持修改操作。如果需要对列表进行修改,应使用new ArrayList<>(List.of(...))将其转换为可变的ArrayList

    • ❌ 错误:对List.of()返回列表调用add
java 复制代码
List<String> list = List.of("apple", "banana");
list.add("cherry");
  • ✅ 正确:需修改时使用new ArrayList<>(List.of(...))
java 复制代码
List<String> list = new ArrayList<>(List.of("apple", "banana"));
list.add("cherry");
  1. 性能优化缺失 :频繁向ArrayList中间插入元素,会导致大量元素移动,性能较低,因为ArrayList是基于数组实现的,在中间插入元素时,需要将插入位置后面的所有元素都向后移动一位。应改用LinkedList(基于链表,插入操作时间复杂度为 O (1))或预计算索引批量操作,减少元素移动次数 。

    • ❌ 错误:频繁向ArrayList中间插入元素
java 复制代码
List<Integer> list = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
    list.add(0, i);
}
  • ✅ 正确:改用LinkedList或预计算索引批量操作
java 复制代码
// 使用LinkedList
List<Integer> list = new LinkedList<>();
for (int i = 0; i < 1000; i++) {
    list.add(0, i);
}

// 预计算索引批量操作
List<Integer> list = new ArrayList<>();
List<Integer> tempList = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
    tempList.add(i);
}
list.addAll(0, tempList);

6.2 最佳实践清单

  1. 泛型声明 :始终使用类型参数(如List<String>而非原始类型List),这样可以在编译期进行类型检查,避免运行时的类型转换异常,提高代码的安全性和可读性。在List<String>中,编译器会确保只能向列表中添加String类型的元素,如果尝试添加其他类型的元素,会在编译时就报错 。

  2. 容量预分配 :已知数据量时通过构造函数指定初始容量(减少扩容开销),ArrayList在初始化时会有一个默认容量,如果后续添加的元素超过这个容量,就会触发扩容操作,扩容操作需要复制原数组到一个更大的新数组中,开销较大。通过指定初始容量,可以减少扩容的次数,提高性能 。

java 复制代码
List<Integer> list = new ArrayList<>(100);
  1. 接口编程 :声明为List而非具体实现类(List<String> list = new ArrayList<>()),这样可以提高代码的可维护性和可扩展性,在后续的开发中,如果需要更换List的实现类,只需要修改构造函数部分的代码,而不需要修改所有使用list的地方 。

  2. 工具类优先 :善用Collections(排序 / 反转)和Arrays(数组转换)提升开发效率,Collections类提供了许多实用的方法,如sort方法可以对List进行排序,reverse方法可以反转List中的元素顺序;Arrays类提供了将数组转换为List的方法,以及其他与数组相关的操作方法 。

java 复制代码
List<Integer> list = new ArrayList<>(Arrays.asList(3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5));
Collections.sort(list); 
Collections.reverse(list); 
String[] array = {"apple", "banana", "cherry"};
List<String> list2 = Arrays.asList(array);
  1. 迭代器优先 :对LinkedList使用Iterator遍历(避免get(index)O(n)开销),LinkedList是基于链表实现的,通过索引访问元素时,需要从头开始遍历链表,时间复杂度为O(n),而使用Iterator遍历可以直接通过链表的指针进行遍历,时间复杂度为O(1)
java 复制代码
List<Integer> list = new LinkedList<>();
list.add(1);
list.add(2);
list.add(3);
Iterator<Integer> iterator = list.iterator();
while (iterator.hasNext()) {
    Integer num = iterator.next();
    System.out.println(num);
}
相关推荐
好好研究3 小时前
Spring Boot - Thymeleaf模板引擎
java·spring boot·后端·thymeleaf
爬山算法3 小时前
Hibernate(76)如何在混合持久化环境中使用Hibernate?
java·后端·hibernate
编程彩机3 小时前
互联网大厂Java面试:从分布式缓存到消息队列的技术场景解析
java·redis·面试·kafka·消息队列·微服务架构·分布式缓存
她说..3 小时前
策略模式+工厂模式实现单接口适配多审核节点
java·spring boot·后端·spring·简单工厂模式·策略模式
f狐0狸x3 小时前
【C++修炼之路】C++ list容器基本用法详解
开发语言·c++·list
松☆3 小时前
Dart 中的常用数据类型详解(含 String、数字类型、List、Map 与 dynamic) ------(2)
数据结构·list
坚持就完事了3 小时前
Java的OOP
java·开发语言
像少年啦飞驰点、3 小时前
零基础入门 Spring Boot:从“Hello World”到可部署微服务的完整学习路径
java·spring boot·微服务·编程入门·后端开发
undsky_4 小时前
【RuoYi-SpringBoot3-Pro】:将 AI 编程融入传统 java 开发
java·人工智能·spring boot·ai·ai编程
不光头强4 小时前
shiro学习要点
java·学习·spring