🚀 Java 巩固进阶 · 第8天
主题:List 接口深度解析 ------ SpringBoot 数据处理的基石
📅 进度概览 :掌握 Java 中最常用的集合类型。
💡 核心价值:
- 数据承载 :Controller 返回给前端的 JSON 数组、MyBatis 查询结果集,90% 都是
List。- 性能抉择 :理解
ArrayListvsLinkedList的底层差异,避免在大数据量场景选错容器导致系统卡顿。- 并发安全 :识别多线程下的
ConcurrentModificationException,掌握线程安全的 List 方案。- 现代编程:结合 Stream API 高效处理 List 数据(过滤、转换、聚合)。
一、List 接口核心特性:有序与索引
List 是 Collection 的子接口,核心特征是 「有序」(插入顺序)、可重复、基于索引。
1. 核心方法速查(索引操作)
| 方法签名 | 作用 | 时间复杂度 (ArrayList) | 备注 |
|---|---|---|---|
void add(int index, E element) |
指定位置插入 | O(n) | 后续元素需移位 |
E get(int index) |
获取指定元素 | O(1) | 随机访问极快 |
E remove(int index) |
删除指定位置元素 | O(n) | 后续元素需前移 |
E set(int index, E element) |
修改指定位置元素 | O(1) | 直接覆盖 |
int indexOf(Object o) |
查找元素首次出现位置 | O(n) | 需遍历比较 |
List<E> subList(int from, int to) |
截取子列表 | O(1) | 返回视图,修改会影响原列表 |
2. 基础代码示例
java
List<String> list = new ArrayList<>();
list.add("A"); // index 0
list.add("B"); // index 1
list.add("C"); // index 2
// 1. 指定插入 (移动元素)
list.add(1, "X"); // [A, X, B, C]
// 2. 随机访问 (极快)
String val = list.get(0);
// 3. 修改 (直接覆盖)
String old = list.set(1, "Y"); // [A, Y, B, C], old="X"
// 4. 删除 (移动元素)
list.remove(2); // [A, Y, C], 删除了"B"
// ⚠️ 注意:subList 返回的是视图,非新集合
List<String> sub = list.subList(0, 2);
sub.add("Z"); // 原 list 也会变成 [A, Y, Z, C]!
二、ArrayList:绝对的主力军
1. 底层原理揭秘
- 数据结构 :动态 Object 数组 (
Object[] elementData)。 - 初始容量:默认 10(懒加载,第一次添加时创建)。
- 扩容机制 :
- 当容量不足时,创建新数组,容量为原来的 1.5 倍 (
old + old/2)。 - 代价 :需要调用
System.arraycopy复制所有元素,频繁扩容会严重损耗性能。 - 优化 :若预估数据量,务必使用构造器指定初始容量
new ArrayList<>(expectedSize)。
- 当容量不足时,创建新数组,容量为原来的 1.5 倍 (
- 线程安全 :不安全 。多线程并发修改会抛出
ConcurrentModificationException或导致数据丢失。
2. 性能画像
| 操作 | 复杂度 | 原因 | 适用场景 |
|---|---|---|---|
| 随机访问 (get/set) | O(1) | 数组内存连续,通过偏移量直接计算地址 | 读多写少,频繁根据索引查询 |
| 尾部添加 (add) | O(1) | 直接放入空位 (均摊) | 日志收集、结果集组装 |
| 中间/头部增删 | O(n) | 需移动大量元素 (arraycopy) |
避免在大数据量头部操作 |
3. 🏆 SpringBoot 应用场景
- API 响应 :
public List<UserDTO> getUserList() - MyBatis 结果 :
List<Order> orders = orderMapper.selectAll();(默认返回 ArrayList) - 配置列表 :
@ConfigurationProperties注入的 List 属性。 - Redis 缓存:存储序列化后的 List 对象。
三、LinkedList:被误解的链表
1. 底层原理揭秘
- 数据结构 :双向链表 (Node:
prev,item,next)。 - 内存特点 :节点分散在堆内存中,不连续,缓存命中率低(CPU 预取失效)。
- 无扩容:动态分配节点,无容量限制。
2. 性能真相(重要!)
| 操作 | 复杂度 | 真相解析 |
|---|---|---|
| 首尾增删 | O(1) | 直接操作 first/last 指针,极快 |
| 随机访问 (get) | O(n) | 必须从头/尾遍历节点,极慢 |
| 中间增删 | O(n) | 误区警示 :虽然删除节点本身是 O(1),但找到该节点需要 O(n) !总耗时 = 查找 + 删除 = O(n)。且在大数据量下,由于缓存不友好,实际表现往往不如 ArrayList。 |
3. 特有方法(双端队列能力)
LinkedList 同时实现了 Deque 接口,可用作栈 或队列。
java
LinkedList<String> queue = new LinkedList<>();
queue.offerFirst("Head"); // 入队头
queue.offerLast("Tail"); // 入队尾
queue.pollFirst(); // 出队头
queue.peekLast(); // 查看队尾
4. ✅ 适用场景
- 频繁的首尾操作:实现 LIFO 栈或 FIFO 队列。
- 无需随机访问:只通过 Iterator 遍历的场景。
- 大数据量头部插入 :如实时日志缓冲(但通常推荐用
ArrayDeque或BlockingQueue)。
四、Vector 与 线程安全方案
1. Vector:历史的尘埃
- 原理 :同步的数组(方法加
synchronized)。 - 缺点 :全表锁,并发度极低,性能差。❌ 严禁在新项目中使用。
2. SpringBoot 中的线程安全 List 方案
若需在多线程环境下使用 List,请选择以下方案:
| 方案 | 实现方式 | 特点 | 适用场景 |
|---|---|---|---|
| CopyOnWriteArrayList | new CopyOnWriteArrayList<>() |
写时复制。读无锁极快,写时拷贝新数组。 | 读多写极少 (如监听器列表、配置列表) |
| Synchronized List | Collections.synchronizedList(new ArrayList<>()) |
包装器,所有方法加锁。 | 写操作较多,且需要强一致性 |
| ConcurrentLinkedQueue | new ConcurrentLinkedQueue<>() |
CAS 算法实现的无锁队列。 | 高并发队列场景 (替代 LinkedList) |
java
// ✅ 推荐:读多写少的安全列表
List<String> safeList = new CopyOnWriteArrayList<>();
safeList.add("A"); // 写操作会拷贝数组,开销大
String val = safeList.get(0); // 读操作无锁,极快
五、性能对比实战与避坑指南
1. 性能测试结论(10万数据)
| 操作 | ArrayList | LinkedList | 赢家 | 原因 |
|---|---|---|---|---|
| 尾部添加 | ⚡ 极快 | ⚡ 快 | 平手 | 差别微乎其微 |
| 随机访问 (get) | ⚡ O(1) | 🐢 O(n) (慢千倍) | ArrayList | 链表需遍历 |
| 中间删除 | 🐢 O(n) (移位) | 🐢 O(n) (查找+删除) | ArrayList | 数组内存连续,CPU 缓存友好;链表指针跳转慢 |
| 头部插入 | 🐢 O(n) | ⚡ O(1) | LinkedList | 数组需移动所有元素 |
💡 黄金法则 :除非你需要频繁在头部 插入/删除,或者明确需要队列/栈 结构,否则无脑选 ArrayList。
2. ⚠️ 常见避坑:遍历时删除元素
错误写法 :直接在 foreach 循环中 remove,会抛 ConcurrentModificationException。
java
// ❌ 报错!
for (String s : list) {
if ("error".equals(s)) {
list.remove(s);
}
}
✅ 正确写法 1:使用 Iterator
java
Iterator<String> it = list.iterator();
while (it.hasNext()) {
if ("error".equals(it.next())) {
it.remove(); // 安全删除
}
}
✅ 正确写法 2:Java 8 removeIf (推荐)
java
list.removeIf(s -> "error".equals(s));
✅ 正确写法 3:倒序遍历 (仅限 ArrayList)
java
for (int i = list.size() - 1; i >= 0; i--) {
if ("error".equals(list.get(i))) {
list.remove(i);
}
}
六、🎯 今日实战任务:Stream API 升级版购物车
背景 :实现一个高性能购物车,不仅要用 List 存储,还要用 Stream API 进行数据处理。
需求步骤
- 定义
Goods类 :- 属性:
id(Long),name(String),price(BigDecimal),count(Integer)。 - 实现
toString,equals,hashCode。
- 属性:
- 购物车管理类
CartService:- 内部使用
private final List<Goods> items = new ArrayList<>();(思考:为什么不用 LinkedList?)。 - 功能方法 :
addItem(Goods goods):若商品已存在,增加数量;否则新增。注意线程安全(假设单线程或外部同步)。removeItem(Long id):根据 ID 删除商品(使用removeIf)。updateCount(Long id, int count):修改数量,若 count<=0 则删除。calculateTotalPrice():核心! 使用 Stream API 计算总价 (mapToDouble->sum)。- 公式:∑(price×count)\sum (price \times count)∑(price×count)
getListByMinPrice(BigDecimal minPrice):使用 Streamfilter筛选价格高于指定值的商品,返回新 List。
- 内部使用
- 测试类 :
- 添加 5 种商品,修改其中一种数量。
- 删除一种商品。
- 打印总价(保留 2 位小数)。
- 筛选价格大于 50 元的商品并打印。
- 挑战 :尝试在遍历
items时直接remove,观察异常,然后改为正确写法。
💡 Stream API 提示
java
import java.math.BigDecimal;
// 计算总价
public BigDecimal calculateTotalPrice() {
return items.stream()
.map(good -> good.getPrice().multiply(BigDecimal.valueOf(good.getCount())))
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
// 筛选
public List<Goods> getListByMinPrice(BigDecimal minPrice) {
return items.stream()
.filter(good -> good.getPrice().compareTo(minPrice) > 0)
.collect(Collectors.toList()); // 或 toList() (Java 16+)
}
📝 第8天 · 核心总结
-
List 选型铁律:
- 默认首选
ArrayList:随机访问快,CPU 缓存友好,适合 95% 的业务场景(查询、遍历、尾部追加)。 - 仅特定场景选
LinkedList:需要频繁首尾增删,或作为栈/队列使用。 - 拒绝
Vector:已过时,性能差。
- 默认首选
-
并发安全:
ArrayList非线程安全。- 读多写少 →
CopyOnWriteArrayList。 - 高并发队列 →
ConcurrentLinkedQueue/BlockingQueue。
-
操作避坑:
- 禁止 在 foreach 循环中直接
remove。 - 使用
Iterator.remove()或list.removeIf()。 - 初始化时尽量指定容量,避免频繁扩容拷贝。
- 禁止 在 foreach 循环中直接
-
现代 Java 风格:
- 处理 List 数据(过滤、统计、转换)优先使用 Stream API,代码更简洁、函数式。
- 金额计算务必使用
BigDecimal,严禁使用double。