4.2 高阶函数与组合:把控制流程变成数据
订单校验常从一串 if 开始。规则增多后,每个分支既判断条件,又拼错误消息,还决定是否继续执行。高阶函数提供的价值不是少写几行循环,而是把“要执行的规则”作为值组织起来。
函数可以被传递和返回
接收函数参数或返回函数的函数,称为高阶函数:
@FunctionalInterface
interface Rule<T> {
Optional<String> validate(T value);
}
static Rule<Order> minimumAmount(BigDecimal minimum) {
return order -> order.total().compareTo(minimum) >= 0
? Optional.empty()
: Optional.of("订单金额不能低于 " + minimum);
}minimumAmount 返回一个捕获了 minimum 的函数。捕获的变量形成闭包。Java 要求局部变量为 final 或 effectively final,这避免闭包观察到该局部绑定之后又被改写,但闭包捕获的对象本身仍可能可变。
Java 在 Lambda 出现之前也能通过匿名类传递行为;Java 8 增加的是 Lambda 表达式、方法引用和配套函数式接口,并非此前“函数绝对不能当参数”。
用组合替代分支扩散
static <T> Rule<T> all(List<Rule<T>> rules) {
return value -> rules.stream()
.map(rule -> rule.validate(value))
.flatMap(Optional::stream)
.findFirst();
}
Rule<Order> checkoutRule = all(List.of(
minimumAmount(new BigDecimal("10.00")),
order -> order.items().isEmpty()
? Optional.of("订单不能为空")
: Optional.empty(),
order -> order.shippingAddress() == null
? Optional.of("缺少收货地址")
: Optional.empty()
));这里的控制策略是“按顺序执行规则,返回第一个错误”。如果业务要一次返回全部错误,可以修改组合器,而不必重写每条规则:
static <T> Function<T, List<String>> collectAll(List<Rule<T>> rules) {
return value -> rules.stream()
.map(rule -> rule.validate(value))
.flatMap(Optional::stream)
.toList();
}高阶函数把变化点从分支内部提到了组合层:单条规则关注事实,组合器关注控制策略。
map、filter、fold 各自回答什么
对集合处理可以先区分三类操作:
map:每个输入元素产生一个输出元素;filter:保留满足条件的元素;fold/reduce:把多个元素合成为一个结果。
BigDecimal revenue = orders.stream()
.filter(order -> order.status() == PAID)
.map(Order::total)
.reduce(BigDecimal.ZERO, BigDecimal::add);这段流水线能直接读成“筛选已支付订单,取金额,再求和”。但链式调用不是天然更清楚。若每个 Lambda 都包含十几行分支,或者需要在阶段间观察复杂状态,应提取有名称的函数,必要时回到普通循环。
惰性求值改变执行时机
Java Stream 的中间操作通常是惰性的,直到终止操作开始才消费数据:
var pipeline = orders.stream()
.filter(Order::isPaid)
.map(Order::total); // 此时尚未遍历
var first = pipeline.findFirst(); // 从这里开始执行,并可能短路惰性能避免不必要的计算,也能处理潜在无限序列;代价是错误和副作用的发生时间不再等同于代码书写顺序。
Java 官方文档要求传给 Stream 的行为参数通常应当无状态,并且不能干扰数据源。实现允许在不影响结果时省略某些阶段,因此不要依赖 map 或 filter 中的日志、计数等副作用一定执行:
// 错误示范:用副作用收集结果
List<String> names = new ArrayList<>();
orders.stream()
.filter(Order::isPaid)
.forEach(order -> names.add(order.customerName()));
// 更清楚:让流水线返回值
List<String> names = orders.stream()
.filter(Order::isPaid)
.map(Order::customerName)
.toList();Stream 不是集合
Java Stream 是一次性消费的计算管道,不是可重复遍历的数据结构:
Stream<Order> paid = orders.stream().filter(Order::isPaid);
long count = paid.count();
// paid.findFirst(); // 不应重用同一个 Stream以 I/O 为数据源的 Stream 还可能持有资源:
try (Stream<String> lines = Files.lines(path)) {
return lines.filter(s -> !s.isBlank()).count();
}把 Stream 存到字段、跨方法传很远或多次消费,都会让生命周期难以判断。通常应在数据源附近创建,在同一作用域内终止。
并行 Stream 不是免费加速
把 .stream() 改成 .parallelStream() 只改变执行策略,不会修复不满足结合律的归约,也不会让阻塞 I/O 自动拥有合理隔离。
并行归约要求组合方式能安全分块再合并。例如加法满足结合律:
(a + b) + c = a + (b + c)浮点加法因舍入并不严格满足数学结合律,所以并行分组可能产生末位不同的结果。涉及顺序、共享状态、线程池竞争或少量廉价元素时,并行开销还可能大于收益。是否采用并行 Stream 应通过基准测试和运行环境评估,而不是按数据量凭感觉开启。
完成检查
把一段“读取订单—过滤—转换—汇总”的代码分别写成普通循环和 Stream:
- 两版都为中间规则命名;
- 不在
map、filter中修改外部集合; - 标出短路行为;
- 若考虑并行,说明归约是否满足结合律;
- 选择更容易维护的一版,并写出理由。
函数式风格的目标是缩小推理范围,不是让每段代码都变成最长的调用链。