跳到正文
Elaine Blog
返回

Lambda、Stream 与副作用边界

Java 基础

Stream 不是“更短的 for 循环”,而是描述数据如何从输入经过变换得到结果。可读的流水线让每一步只做一件事,并把 数据库写入、日志等副作用放到边界之外。

Table of contents

Open Table of contents

写一条可测试的数据管道

import java.math.BigDecimal;
import java.util.*;

record 订单(String customerId, BigDecimal amount, boolean paid) {}

static Map<String, BigDecimal> 已支付金额按客户汇总(List<订单> orders) {
    return orders.stream()
        .filter(订单::paid)
        .collect(java.util.stream.Collectors.groupingBy(
            订单::customerId,
            java.util.stream.Collectors.reducing(
                BigDecimal.ZERO, 订单::amount, BigDecimal::add)));
}

filter 是中间操作,collect 是终止操作。Stream 具有惰性求值(Lazy Evaluation):在终止操作出现前,中间操作 通常不会执行。一个 Stream 只能消费一次;需要再次处理时重新从数据源创建。

Lambda 的本质

Lambda 表达式是函数式接口实例的简写。函数式接口只有一个抽象方法,例如 Predicate<T> 接收值并返回布尔结果, Function<T,R> 把一种值转换成另一种值。

java.util.function.Predicate<订单> 已支付 = 订单::paid;
java.util.function.Function<订单, BigDecimal> 取金额 = 订单::amount;

方法引用 订单::paid 并不天然比 Lambda 更好;只有当名称能准确表达意图时使用。复杂规则提取成命名方法,避免在 流水线里塞入大段条件。

把副作用移出流水线

**副作用(Side Effect)**是函数除返回值外对外部状态产生的变化,例如修改共享列表、写数据库或发送消息。

var ids = new ArrayList<String>();
orders.stream().filter(订单::paid).forEach(o -> ids.add(o.customerId())); // 不推荐

改成返回新值:

var ids = orders.stream().filter(订单::paid).map(订单::customerId).toList();

这样更容易测试,也不会在切换并行执行后产生数据竞争。

不要随手调用 parallel

并行流使用共享的 ForkJoinPool。它可能适合数据量足够大、任务 CPU 密集、每项成本相近且没有副作用的场景。数据库 调用、HTTP 请求和小集合通常不适合:阻塞会占住公共线程,拆分与合并开销可能大于收益。

决定并行前应使用 JMH 等基准工具测量顺序和并行版本,并记录数据规模、预热、CPU 核数与分位耗时。不能用一次 System.nanoTime() 得出生产结论。

可读性停止线

流水线超过约五六步、需要多次维护状态或包含复杂分支时,命名中间结果或改回普通循环往往更清楚。Stream 是工具, 不是代码风格考核。

下一步

阅读文件、网络与 NIO 基础,理解数据管道与真实 I/O 的边界。


分享这篇文章:

上一篇
泛型、边界与类型擦除
下一篇
文件、网络与 NIO 基础