8年Java开发,踩过的坑比写过的代码还多。今天聊两个让我线上P0过、半夜起来修过的问题:Stream遍历时莫名其妙抛ConcurrentModificationException,以及Lambda表达式“以为很优雅,结果内存泄漏” 。
一、Stream流遍历的“隐形雷”:ConcurrentModificationException
1. 现象:代码看起来没问题,运行就报错
java
List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C"));
list.stream().forEach(item -> {
if ("B".equals(item)) {
list.remove(item); // ❌ 这里会抛异常
}
});
异常信息:
text
java.util.ConcurrentModificationException
2. 为什么?——Stream遍历时不能修改原集合
Stream底层使用了迭代器(Iterator) ,遍历过程中会维护一个modCount(修改次数计数器)。
当你用list.remove()直接修改原集合时,modCount变了,但迭代器不知道,于是抛异常。
简单记:Stream只读,不能边遍历边增删改原集合。
3. 正确做法(3种,按场景选)
✅ 方案1:用 removeIf(推荐,最简洁)
java
list.removeIf("B"::equals);
✅ 方案2:收集要删除的元素,遍历完后统一删
java
List<String> toRemove = new ArrayList<>();
list.stream().forEach(item -> {
if ("B".equals(item)) {
toRemove.add(item);
}
});
list.removeAll(toRemove);
✅ 方案3:用 Iterator 手动遍历
java
Iterator<String> it = list.iterator();
while (it.hasNext()) {
if ("B".equals(it.next())) {
it.remove(); // ✅ 安全
}
}
4. 扩展陷阱:filter 里也不能直接删
java
// ❌ 错误
list.stream()
.filter(item -> {
if ("B".equals(item)) list.remove(item);
return true;
})
.collect(Collectors.toList());
filter 里做副作用操作(删除、修改原集合)同样是雷。
二、Lambda表达式的“内存泄漏”:你以为优雅,其实GC回收不掉
1. 现象:服务跑几天就OOM,堆dump里全是某个匿名类
2. 为什么?——Lambda捕获了外部引用
Lambda表达式如果捕获了外部对象的引用,并且这个Lambda被长期持有(比如放进静态集合、线程池、监听器),那么外部对象也无法被GC回收。
典型错误场景:
java
public class Handler {
private static List<Runnable> tasks = new ArrayList<>();
public void addTask() {
// ❌ 这里捕获了 this(整个Handler实例)
Runnable r = () -> System.out.println(this.toString());
tasks.add(r); // tasks是static,永远不释放,导致Handler无法GC
}
}
3. 真实线上案例(我踩过的)
一个Spring Boot项目,在循环里用Lambda往
ExecutorService提交任务,每个Lambda都捕获了当前HttpServletRequest对象。
请求结束后,这些Lambda还留在队列里,导致Request对象无法回收。
跑了3天,老年代占满,Full GC频繁,最终OOM。
4. 正确做法(3条原则)
✅ 原则1:避免捕获不必要的this
java
// ❌ 隐式捕获this
Runnable r = () -> System.out.println(this.field);
// ✅ 用局部变量
String field = this.field;
Runnable r = () -> System.out.println(field);
✅ 原则2:Lambda里不要持有大对象引用
java
// ❌ 捕获整个request
request.getParameterMap();
// ✅ 只取需要的数据
String param = request.getParameter("id");
Runnable r = () -> process(param);
✅ 原则3:不要将Lambda长期存放到静态集合/线程池中
java
// ❌ 静态List存Lambda
private static List<Callable<String>> cache = new ArrayList<>();
// ✅ 改用普通局部变量,或及时清理
5. 如何排查Lambda内存泄漏?
- dump堆:
jmap -dump:live,format=b,file=heap.hprof <pid> - 用MAT(Eclipse Memory Analyzer)打开,查看
Leak Suspects - 搜索
$$Lambda$类,看哪些被大量实例化且无法回收 - 追踪这些Lambda的引用链,找到谁在持有它们
三、总结:两个“优雅陷阱”的保命口诀
| 新特性 | 错误用法 | 正确姿势 |
|---|---|---|
| Stream | 遍历时 remove/add 原集合 | removeIf、Iterator.remove()、先收集后删除 |
| Lambda | 长期持有捕获外部引用的Lambda | 避免捕获this、避免存静态集合、用局部变量替代 |
一句话记:
Stream只读不写,Lambda用完即扔。
四、互动一下
你在项目中遇到过哪些“以为很优雅,结果线上炸了”的Java 8写法?
评论区见,一起避雷👇
如果你觉得有用,点个赞/收藏,产假期间我会继续输出Java实战踩坑系列。