Java List集合深度解析:从基础用法到实战技巧全攻略
一、List 集合核心概念与特性

1.1 List 接口定义与本质
List 是 Java 集合框架中Collection接口的子接口,代表有序的元素序列。其核心特性包括:
-
有序性:元素按插入顺序存储,索引从 0 开始。例如,依次向 List 中添加元素 A、B、C,那么通过索引获取时,顺序也为 A、B、C。
-
可重复性 :允许存储相同元素(
equals()方法返回true)。比如,一个 List 中可以多次添加字符串 "hello"。 -
动态扩容 :实现类自动管理容量(如
ArrayList)。当添加元素超过当前容量时,会自动扩大内部数组的大小,以容纳更多元素。 -
泛型支持 :通过
List<E>保证类型安全。在定义 List 时指定元素类型,如List<Integer>,这样在编译期就能检查类型错误,避免运行时的类型转换异常。
1.2 与数组的本质区别
| 特性 | List | 数组 |
|---|---|---|
| 长度 | 动态可变(通过扩容机制) | 固定(初始化后不可变) |
| 元素类型 | 支持泛型(编译期类型检查) | 基本类型 / 引用类型数组 |
| 操作方法 | 丰富的 API(增删改查迭代器) | 仅通过索引访问 |
| 内存分配 | 自动管理内存(内部数组扩容) | 需手动分配连续内存空间 |
从长度上看,数组在创建时就确定了大小,后续无法改变;而 List 的大小可以根据元素的添加和删除动态变化。在元素类型方面,数组可以是基本类型或引用类型,而 List 只能存储引用类型,通过泛型来指定具体的对象类型,增强了类型安全性。操作方法上,List 提供了如add、remove、contains、iterator等丰富的方法来操作元素,而数组主要通过索引进行访问和赋值。内存分配上,数组需要手动指定大小并分配连续的内存空间,而 List 内部自动管理内存,通过动态扩容来适应元素数量的变化 。
二、核心实现类对比与选型指南
2.1 四大主流实现类详解
2.1.1 ArrayList:数组驱动的通用列表
-
数据结构 :基于动态数组实现(
Object[] elementData),元素在内存中连续存储,实现了RandomAccess接口,支持快速随机访问。 -
性能特点:
-
随机访问(
get(index)):时间复杂度为 O (1),因为可以直接通过数组下标进行寻址,就像在一排编好号的柜子中,直接根据编号就能快速找到对应的柜子。 -
中间插入 / 删除(
add(index, e)):时间复杂度为 O (n),因为在中间位置插入或删除元素时,需要移动后续的元素。例如,在一个队伍中间插入一个人,后面的人都需要往后移动一个位置。 -
尾部操作(
add(e)):均摊时间复杂度为 O (1) ,通常情况下,在尾部添加元素很快,只有当内部数组容量不足,触发扩容时,才需要进行数组复制,将原数组元素复制到新的更大的数组中 。
-
-
适用场景:
-
默认首选 :在大多数情况下,如果没有特殊的性能要求,
ArrayList是一个很好的选择,因为它综合性能较好,使用简单。 -
频繁读取、尾部追加场景 :比如在数据库结果集封装时,从数据库中查询出一批数据,需要将这些数据存储在一个集合中,然后进行后续的处理。由于查询结果通常是顺序获取的,并且后续可能只是对这些数据进行读取操作,所以使用
ArrayList非常合适 。
-
2.1.2 LinkedList:双向链表的高效变更
-
数据结构 :双向链表(每个节点包含
prev和next指针),每个节点不仅知道下一个节点的位置,还知道上一个节点的位置,这使得双向遍历成为可能。 -
性能特点:
-
随机访问:时间复杂度为 O (n),因为需要从头开始遍历链表,直到找到目标索引的节点。这就好比在一条长街上找一个门牌号,必须从街头一个一个门牌号数过去。
-
中间插入 / 删除:时间复杂度为 O (1),在已知插入或删除位置的情况下,只需修改相关节点的指针引用即可,不需要移动大量元素。例如,在一个环形的队伍中,让两个人交换位置,只需要调整他们与相邻人的牵手关系。
-
内存占用 :高于
ArrayList,因为每个节点除了存储数据本身,还需要额外存储两个指针,分别指向前一个节点和后一个节点。
-
-
适用场景:
- 频繁中间插入 / 删除场景 :比如实现队列的
offer/poll操作,队列是一种先进先出的数据结构,LinkedList可以高效地在头部和尾部进行插入和删除操作,非常适合实现队列。再比如实现撤销操作历史记录,每一次操作都可以作为一个节点添加到链表中,当需要撤销时,直接从链表中删除最后一个节点即可。
- 频繁中间插入 / 删除场景 :比如实现队列的
2.1.3 Vector:线程安全的历史选择
-
数据结构 :基于数组实现(与
ArrayList类似),内部也是通过一个数组来存储元素。 -
核心特性:
-
方法同步 :所有操作通过
synchronized修饰,这使得Vector在多线程环境下是线程安全的,但同时也带来了性能损耗,大约会有 20%-30% 的性能下降,因为加锁会导致线程之间的竞争和等待。 -
扩容策略 :默认扩容为原容量的 2 倍,而
ArrayList默认扩容为原容量的 1.5 倍。例如,当Vector的当前容量为 10,需要扩容时,新的容量会变为 20。
-
-
适用场景:
- 遗留系统兼容 :在一些早期的 Java 系统中,可能会使用
Vector来保证线程安全。但在新的开发场景中,由于其性能问题,通常建议使用Collections.synchronizedList(new ArrayList<>())来代替,这种方式可以在需要线程安全的情况下,通过对ArrayList进行包装,实现线程安全,同时又能利用ArrayList的性能优势。
- 遗留系统兼容 :在一些早期的 Java 系统中,可能会使用
2.1.4 CopyOnWriteArrayList:高并发下的读写分离
-
数据结构:写时复制(每次写操作创建新数组副本),当有写操作发生时,会先复制一份原数组,然后在新的数组上进行修改,最后将原数组的引用指向新数组。
-
并发特性:
-
读操作 :不加锁,直接访问原数组,因此读操作非常快,适用于 99% 读场景,比如在一个高并发的系统中,大量的线程只是读取配置信息,而很少有线程去修改配置,使用
CopyOnWriteArrayList可以大大提高读取性能。 -
写操作:加锁并复制数组,这使得写操作的开销较大,所以适用于多读少写场景。在写操作时,先获取锁,然后复制数组,在新数组上进行修改,最后释放锁。
-
迭代器:基于快照机制,迭代器在创建时会获取当前数组的一个快照,在迭代过程中不会反映后续的写操作,这保证了迭代过程的一致性,但也意味着迭代器可能读取到的不是最新的数据。
-
-
适用场景:
- 高并发读取 :如日志监控系统,大量的线程需要读取日志信息进行分析,而写操作相对较少,使用
CopyOnWriteArrayList可以提高读取性能,保证系统的高效运行。再比如配置信息缓存,配置信息通常很少修改,但会被多个线程频繁读取,使用CopyOnWriteArrayList可以满足这种高并发读取的需求。
- 高并发读取 :如日志监控系统,大量的线程需要读取日志信息进行分析,而写操作相对较少,使用
2.2 选型决策树
在实际应用中,选择合适的List实现类可以显著提升程序性能。以下是一个简单的选型决策树:
plantuml
@startmindmap
* 是否需要线程安全?
** 是
*** 是否读多写少?
**** 是: CopyOnWriteArrayList
**** 否: Collections.synchronizedList(new ArrayList<>()) 或 Vector(不推荐)
** 否
*** 是否频繁随机访问?
**** 是: ArrayList
**** 否
***** 是否频繁中间插入/删除?
****** 是: LinkedList
****** 否: ArrayList
@enduml
-
首先判断是否需要线程安全。如果需要,再看是否是读多写少的场景,如果是,选择
CopyOnWriteArrayList;如果不是,选择Collections.synchronizedList(new ArrayList<>()),尽量避免使用Vector。 -
如果不需要线程安全,接着判断是否频繁进行随机访问。如果是,选择
ArrayList;如果不是,再判断是否频繁进行中间插入 / 删除操作,如果是,选择LinkedList;如果不是,也选择ArrayList。
三、基础操作与核心 API 详解
3.1 创建与初始化最佳实践
3.1.1 标准初始化方式
- 常规构造器初始化 :使用
ArrayList或LinkedList的无参构造器,后续通过add方法逐个添加元素。
java
List<String> list1 = new ArrayList<>();
list1.add("apple");
list1.add("banana");
- Arrays.asList() :将数组转换为
List,但返回的List是固定大小的,不支持添加或删除元素操作(会抛出UnsupportedOperationException)。
java
String[] array = {"apple", "banana"};
List<String> list2 = Arrays.asList(array);
- List.of()(Java 9+) :创建不可变
List,简洁高效,不允许null元素,且不支持修改操作。
java
List<String> list3 = List.of("apple", "banana");
- Stream 初始化 :利用 Java 8 的
StreamAPI 创建List,适合链式操作和复杂数据生成场景。
java
List<String> list4 = Stream.of("apple", "banana")
.collect(Collectors.toList());
3.1.2 初始化陷阱
- List.of () 的不可修改性 :
List.of()创建的列表是不可修改的,尝试调用add或remove方法会抛出UnsupportedOperationException异常。这是因为List.of()返回的是一个不可变的列表实现,其设计目的就是为了提供只读的列表视图,确保数据的不可变性,常用于创建常量列表等场景。
java
List<String> list = List.of("a", "b", "c");
// 以下操作会抛出UnsupportedOperationException
list.add("d");
list.remove("a");
- 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 添加操作
- add(E e) :将元素追加到列表末尾,
ArrayList的均摊时间复杂度为 O (1),LinkedList为 O (1)。
java
List<Integer> list = new ArrayList<>();
list.add(1);
- add(int index, E element) :在指定索引处插入元素,
ArrayList时间复杂度为 O (n)(需移动后续元素),LinkedList为 O (1)(修改指针)。
java
list.add(1, 2);
- addAll(Collection<? extends E> c):将另一个集合的所有元素添加到当前列表末尾,时间复杂度与被添加集合的大小相关。
java
List<Integer> anotherList = Arrays.asList(3, 4);
list.addAll(anotherList);
3.2.2 删除操作
- remove(Object o) :删除指定元素的第一个匹配项,
ArrayList时间复杂度为 O (n)(需查找并移动元素),LinkedList为 O (n)(需查找并修改指针)。
java
list.remove(Integer.valueOf(2));
- remove(int index) :删除指定索引处的元素,
ArrayList时间复杂度为 O (n),LinkedList为 O (n)。
java
list.remove(0);
- removeAll(Collection<?> c):删除当前列表中包含在指定集合中的所有元素,时间复杂度与两个集合的大小相关。
java
List<Integer> removeList = Arrays.asList(3, 4);
list.removeAll(removeList);
3.2.3 修改与查询
- set(int index, E element) :替换指定索引处的元素,返回被替换的旧元素,
ArrayList和LinkedList时间复杂度均为 O (1)(LinkedList需先定位到节点)。
java
Integer oldValue = list.set(0, 5);
- get(int index) :获取指定索引处的元素,
ArrayList时间复杂度为 O (1),LinkedList为 O (n)。
java
Integer element = list.get(0);
- indexOf(Object o):返回指定元素首次出现的索引,若不存在则返回 -1,时间复杂度为 O (n)。
java
int index = list.indexOf(5);
- 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 流式处理 | 函数式操作(过滤 / 映射) | 数据转换与聚合 | 并行流支持多核优化 |
- 普通 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);
}
- 增强 for 循环 :语法简洁,适用于简单遍历,无需关心索引。其底层使用
Iterator实现,性能与Iterator遍历基本相同,时间复杂度为 O (n)。
java
for (Integer num : list) {
System.out.println(num);
}
- 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);
}
- 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);
}
- Stream 流式处理 :提供函数式编程风格,支持链式操作,如过滤、映射、聚合等。可以使用并行流(
parallelStream())充分利用多核 CPU 进行并行处理,提高处理大数据集的效率,时间复杂度根据具体操作而定 。
java
list.stream()
.filter(num -> num > 2)
.map(num -> num * 2)
.forEach(System.out::println);
3.3.2 遍历陷阱
- 普通 for 循环删除元素 :在普通 for 循环中删除元素会导致索引混乱,因为删除元素后,后续元素会向前移动,索引会发生变化。如果继续按照原索引进行遍历,会跳过一些元素。正确的做法是使用
Iterator的remove()方法来删除元素。
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);
- 并发遍历异常 :在并发环境下,如果多个线程同时对
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 排序策略
- 自然排序 :元素实现
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);
- 自定义排序 :使用
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);
- 逆序排序 :使用
Collections.reverseOrder()获取逆序Comparator,或在自定义Comparator中反转比较逻辑。
java
list.sort(Collections.reverseOrder());
System.out.println(list);
4.1.2 数据转换
-
List 与数组互转:
- List 转数组 :调用
toArray()方法,可指定目标数组类型。
- List 转数组 :调用
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);
-
List 与 Set 互转:
- List 转 Set :利用
HashSet或TreeSet构造器,可去除重复元素。
- List 转 Set :利用
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 子列表与视图操作
- 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);
-
视图操作注意事项:
-
生命周期:子列表视图的生命周期依赖于原列表,原列表销毁时,子列表也不可用。
-
结构修改:在原列表进行结构修改(如删除元素)可能导致子列表不可预测行为,尽量在子列表上进行修改操作 。
-
4.3 并发场景处理
-
线程安全的 List 实现:
-
Vector :所有方法同步,性能较低,已逐渐被弃用。在早期的 Java 系统中,
Vector被广泛用于多线程环境,但由于其所有方法都使用synchronized关键字进行同步,导致在高并发场景下,线程竞争激烈,性能下降明显 。 -
Collections.synchronizedList(List list) :包装非线程安全
List,手动同步迭代器操作,在遍历List时,需要使用synchronized块来同步对迭代器的访问,以避免ConcurrentModificationException异常 。 -
CopyOnWriteArrayList:写时复制,适合读多写少场景,读操作不加锁,写操作加锁并复制数组 。
-
-
避免并发修改异常:
-
使用迭代器安全删除 :在多线程环境下,使用
Iterator的remove()方法删除元素,而不是直接调用List的remove()方法,以避免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参数。
- 使用
@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>。
- 使用
@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 策略)
使用LinkedList和HashMap实现一个简单的 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 六大高频错误
-
遍历删除错误 :在使用增强 for 循环遍历
List时直接删除元素,会导致ConcurrentModificationException异常,因为增强 for 循环底层使用Iterator,在遍历过程中修改集合结构会触发fail - fast机制。正确的做法是使用Iterator的remove方法进行删除操作,这样可以保证在删除元素时,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();
}
}
-
类型比较错误 :使用
==比较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");
}
-
未初始化直接使用 :声明
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");
-
忽略线程安全 :在多线程环境下直接使用
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();
- ✅ 正确:根据场景选择
synchronizedList或CopyOnWriteArrayList
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();
-
误用不可变列表 :对
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");
-
性能优化缺失 :频繁向
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 最佳实践清单
-
泛型声明 :始终使用类型参数(如
List<String>而非原始类型List),这样可以在编译期进行类型检查,避免运行时的类型转换异常,提高代码的安全性和可读性。在List<String>中,编译器会确保只能向列表中添加String类型的元素,如果尝试添加其他类型的元素,会在编译时就报错 。 -
容量预分配 :已知数据量时通过构造函数指定初始容量(减少扩容开销),
ArrayList在初始化时会有一个默认容量,如果后续添加的元素超过这个容量,就会触发扩容操作,扩容操作需要复制原数组到一个更大的新数组中,开销较大。通过指定初始容量,可以减少扩容的次数,提高性能 。
java
List<Integer> list = new ArrayList<>(100);
-
接口编程 :声明为
List而非具体实现类(List<String> list = new ArrayList<>()),这样可以提高代码的可维护性和可扩展性,在后续的开发中,如果需要更换List的实现类,只需要修改构造函数部分的代码,而不需要修改所有使用list的地方 。 -
工具类优先 :善用
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);
- 迭代器优先 :对
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);
}