299. Java Stream API - 逐个消费流元素:使用 forEach() 方法
🎯 forEach() 是什么?
forEach() 是一个终止操作,适用于 将流中的每个元素传递给一个 Consumer(消费者)来进行处理。
👀 它通常用于对每个元素执行副作用操作,比如打印、写日志、发送网络请求等。
✅ 示例:打印所有符合条件的元素
import java.util.stream.Stream;
public class ForEachExample {
public static void main(String[] args) {
Stream<String> strings = Stream.of("one", "two", "three", "four");
strings.filter(s -> s.length() == 3)
.map(String::toUpperCase)
.forEach(System.out::println);
}
}
输出结果:
ONE
TWO
⚠️ 注意!forEach() 用得爽,可能也藏坑!
虽然 forEach() 看起来很简单,但它背后隐藏的问题是 “副作用(side-effect)”。
什么是副作用?
当 Lambda 表达式改变了它之外的状态,比如:
- 修改外部变量;
- 向集合中添加元素;
- 改变字段、写文件、打印控制台等等。
⚠️ 副作用例子(容易出问题的写法)
import java.util.*;
import java.util.stream.Stream;
public class SideEffectExample {
public static void main(String[] args) {
Stream<String> strings = Stream.of("one", "two", "three", "four");
List<String> result = new ArrayList<>();
strings.filter(s -> s.length() == 3)
.map(String::toUpperCase)
.forEach(result::add); // ❌ 有副作用!
System.out.println("result = " + result);
}
}
输出:
result = [ONE, TWO]
❗ 为什么这种写法有风险?
-
副作用 = 捕获外部变量 = 捕获式 Lambda
→ 性能下降,JVM生成额外对象。 -
并发不安全
→ 如果使用parallelStream()并发处理,多个线程同时写入ArrayList会出错或数据错乱。 -
副作用让代码难以测试、难以维护
→ 你很难保证副作用带来的状态是否正确,尤其在并发环境中。
✅ 更推荐的做法:无副作用地收集元素
✅ 使用 toList() 方法(Java 16+)
import java.util.List;
import java.util.stream.Stream;
public class SafeCollectExample {
public static void main(String[] args) {
Stream<String> strings = Stream.of("one", "two", "three", "four");
List<String> result = strings
.filter(s -> s.length() == 3)
.map(String::toUpperCase)
.toList(); // ✅ 无副作用
System.out.println("result = " + result);
}
}
输出:
result = [ONE, TWO]
✅ 使用 Collectors.toList()(适用于 Java 8+)
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class CollectorsExample {
public static void main(String[] args) {
Stream<String> strings = Stream.of("one", "two", "three", "four");
List<String> result = strings
.filter(s -> s.length() == 3)
.map(String::toUpperCase)
.collect(Collectors.toList()); // ✅ 无副作用
System.out.println("result = " + result);
}
}
🔍 对比:三种方式对比总结
| 方法 | 是否线程安全 | 是否有副作用 | 可读性 | 备注 |
|---|---|---|---|---|
forEach(result::add) |
❌ 否 | ✅ 有 | ✅ 简单 | 不推荐;并发风险+性能损耗 |
.collect(Collectors.toList()) |
✅ 是 | ❌ 无 | ✅ 清晰 | 适用于 Java 8+,推荐使用 |
.toList() |
✅ 是 | ❌ 无 | ✅ 简洁 | Java 16+,结果为不可变列表 |
🚀 小结与最佳实践建议
✅ forEach() 最适合:
- 调试(打印、日志)
- 执行单个操作(写数据库、发请求)
- 你清楚知道副作用不会引发问题的情况
❌ 不推荐使用 forEach() 来:
- 将结果“收集”到集合中(请使用
.collect()或.toList()) - 执行可能会在并发环境下出错的操作
🎁 结语一句话:
能用
collect()就别用forEach()+ 副作用收集,能避免副作用就避免副作用!