《Effective Java》解读第46条:优先选择Stream中无副作用的函数

第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 做延迟执行、短路优化、合并操作。有副作用会让所有优化失效。

  • 无时序依赖

相关推荐
h39741 小时前
DirectShow过滤器开发-视频源过滤器
c++·windows·音视频·directshow
代码什么用4 小时前
Spring基础使用
java·后端·spring
Su米苏5 小时前
Spring AI 中MCP 与普通 @Tool的区别
java·人工智能·spring
码云数智-园园5 小时前
unique_ptr 还是 shared_ptr?C++ 智能指针选型与内存泄漏实战分析
java·开发语言
老板一杯拿铁5 小时前
Java 异常体系详解:从字节码原理到最佳实践
java
蜗牛互联网5 小时前
Java 17 HttpClient调用文件转写API的超时与失败回退
java·人工智能·后端
禾小西6 小时前
07丨Redis 哨兵机制:主库故障后,如何恢复服务?
java·开发语言·redis
我不是阵雨6 小时前
JDK 21虚拟线程Pinning陷阱:一文拔钉解困
java·开发语言
全栈练习生6 小时前
AI Agent 沙箱
python·ai
小卡车5556 小时前
java反射、自定义注解的应用(自定义分页)
java