第46条:优先选择Stream中无副作用的函数
在 Java Stream 流水线中,传给中间操作、终端操作的函数,必须是无副作用的------ 函数只做计算 / 映射 / 过滤,不修改外部变量、不读写外部状态、不产生额外行为。
什么是「副作用」?
函数执行时,除了返回一个结果,还偷偷修改了外部世界。
修改外部变量(count++、list.add())
读写文件、网络、数据库
打印日志、控制台输出
修改静态变量、共享对象状态
Stream 设计初衷是声明式、并行安全、可复用、易理解,副作用会彻底破坏这些特性。
例子
反例:
java
// 错误示例:有副作用,破坏 Stream 设计
List<String> words = Arrays.asList("apple", "banana", "cat");
int count = 0;
// 错误:lambda 里修改外部 count,产生副作用
words.stream()
.filter(w -> w.length() > 3)
.forEach(w -> count++); // 副作用:修改外部变量
System.out.println(count);
- 修改了外部变量count,当切换为parallelStream()时,会产生并发问题。
- 副作用在lambda中,难以排查和维护。
- forEach 应该只应用于最终消费(如打印、存库),不应用于计算。
正例:
java
// 正确:无副作用,纯声明式计算
List<String> words = Arrays.asList("apple", "banana", "cat");
long count = words.stream()
.filter(w -> w.length() > 3)
.count(); // 终端操作直接返回结果,无外部修改
只做输入->输出,不触碰外部变量。
经典使用例子:
java
// ❌ 错误(副作用:收集时修改外部 List)
List<String> result = new ArrayList<>();
words.stream()
.map(String::toUpperCase)
.forEach(result::add); // 副作用:修改外部list
// ✅ 正确(无副作用,使用收集器)
List<String> result = words.stream()
.map(String::toUpperCase)
.collect(Collectors.toList());
✅ 允许的行为(无副作用)
纯计算:map(x -> x*2)
纯判断:filter(x -> x>10)
纯提取:map(Student::getName)
使用标准收集器:collect(toList())、count()、sum()
groupingBy 可以分组 + 下游收集器,实现分组统计
counting () 只能放分组里,不要直接用
求和、平均、计数优先用 stream 原生方法,不要用 collect
groupingBy 有 3 个版本,可指定 Map 类型
partitioningBy 分成 true/false
parallel 用 groupingByConcurrent
❌ 禁止的行为(有副作用)
lambda 里修改外部变量、集合
lambda 里 IO 操作(打印、读写文件)
lambda 里修改对象内部状态
用 forEach 做计算 / 收集
为什么最好选择无副作用方法?
-
并行安全
Stream 可以轻松并行,但有副作用的函数在并行下一定会出错(竞态条件、数据丢失)。无副作用函数天然线程安全。
-
可读性强
声明式代码:我要什么,而不是我要怎么做。别人一眼看懂:过滤→映射→收集,没有隐藏逻辑。
-
可优化、可复用
JVM 可以对无副作用的 Stream 做延迟执行、短路优化、合并操作。有副作用会让所有优化失效。
-
无时序依赖