CodeRoad Map 程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。
由 程序员柏风 独立维护 · 关于作者
学习 更多
编程范式:函数式、结构化与反应式
这一课你将能做到
在判题平台代码里识别命令式、函数式、反应式三种风格,并知道各自适用场景
用 Java 17 的 Stream、Optional、Function、Predicate 写出无副作用的判题统计代码
用 Reactor Mono/Flux 写一个不阻塞主线程的判题回调
为什么要学
命令式("先干啥后干啥")是 Java 的起点,但写多了你会发现:循环嵌套深、临时变量多、并发难写。函数式("数据流 + 不可变")让集合处理、判题统计、规则组合变得简洁;反应式("事件流 + 非阻塞")让高并发判题回调、WebSocket 推送不再吃线程。本课给初学者一个范式地图,不再"只会 for 循环"。
先建立图像
命令式 函数式 反应式
───────────── ───────────── ─────────────
for (...) { stream.map() Flux.fromIterable(...)
if (...) {...} .filter() .flatMap(...)
list.add(...) .collect() .subscribe(...)
}
线程阻塞等待 不可变、无副作用 非阻塞、事件驱动
跟着做
步骤 1:命令式 vs 函数式
需求:统计某课程下 AC 率(通过提交数 / 总提交数)。
命令式(Java 8 前) :
public double acceptanceRate(List<Submission> submissions) {
int total = 0;
int accepted = 0;
for (Submission s : submissions) {
if (s.getStatus() != SubmissionStatus.PENDING) {
total++;
if (s.getStatus() == SubmissionStatus.ACCEPTED) {
accepted++;
}
}
}
return total == 0 ? 0.0 : (double) accepted / total;
}
函数式(Java 17) :
public double acceptanceRate(List<Submission> submissions) {
var finished = submissions.stream()
.filter(s -> s.getStatus() != SubmissionStatus.PENDING)
.toList();
long accepted = finished.stream()
.filter(s -> s.getStatus() == SubmissionStatus.ACCEPTED)
.count();
return finished.isEmpty() ? 0.0 : (double) accepted / finished.size();
}
差别:函数式不修改外部状态(无副作用)、链式表达数据流、容易并行化(.parallel())。
步骤 2:Optional 消灭 null 判断
// 反例:层层 if null
public String getUserDisplayName(Long userId) {
CfUser user = userMapper.selectById(userId);
if (user != null) {
if (user.getNickname() != null) {
return user.getNickname();
}
}
return "匿名";
}
// 正解:Optional 链式
public String getUserDisplayName(Long userId) {
return Optional.ofNullable(userMapper.selectById(userId))
.map(CfUser::getNickname)
.orElse("匿名");
}
Optional 强迫你思考"空怎么办",且 map/orElse/orElseThrow 让逻辑链一目了然。
步骤 3:用 Predicate 组合校验规则 public class SubmissionRules {
public static final Predicate<Submission> CODE_NOT_BLANK =
s -> !s.getCode().isBlank();
public static final Predicate<Submission> CODE_LENGTH_OK =
s -> s.getCode().length() <= 100_000;
public static final Predicate<Submission> LANGUAGE_SUPPORTED =
s -> Set.of("java", "python", "cpp").contains(s.getLanguage());
public static final Predicate<Submission> ALL =
CODE_NOT_BLANK.and(CODE_LENGTH_OK).and(LANGUAGE_SUPPORTED);
}
// 使用
if (!SubmissionRules.ALL.test(submission)) {
throw new IllegalArgumentException("提交校验失败");
}
步骤 4:反应式判题回调 判题引擎异步回调,用 Reactor 写非阻塞处理:
public Mono<Void> onJudgeCallback(JudgeResult result, long submissionId) {
return Mono.fromCallable(() -> submissionMapper.selectById(submissionId))
.subscribeOn(Schedulers.boundedElastic()) // JDBC 是阻塞的,切到弹性线程池
.flatMap(submission -> {
submission.markAs(result.isAccepted() ? ACCEPTED : REJECTED);
return Mono.fromRunnable(() -> submissionMapper.updateById(submission))
.subscribeOn(Schedulers.boundedElastic())
.then(notifyUser(submission));
});
}
private Mono<Void> notifyUser(Submission s) {
// WebSocket 推送,本身是非阻塞的
return webSocketSession.send(s.getUserId(), "判题完成");
}
主线程不等待,事件来了就处理,适合高并发判题回调。
讲清楚要点
命令式 :告诉机器每一步怎么做,状态可变,适合业务流转和 IO。
函数式 :数据流 + 不可变 + 无副作用,适合集合处理、规则组合、并发计算。
反应式 :事件流 + 非阻塞 + 背压,适合高并发 IO、消息回调、WebSocket。
Java 的函数式武器 :Stream、Optional、Function、Predicate、Consumer、record、sealed。
副作用是万恶之源 :map 里改全局变量、filter 里调 RPC,都是函数式反模式。
不要范式狂热 :该命令式的地方(写库、改状态)别硬套 Stream;该函数式的地方(统计、转换)别写 for 循环。
你可能会懵的地方
Stream 一定比 for 循环慢吗?
对简单循环可能略慢(对象创建开销),但可读性、并行化收益远超那点损耗。真有性能瓶颈再上 JMH 测,不要预先优化。
Optional 能当字段吗?
不能。Optional 只用于返回值;字段和参数别用 Optional,用 @Nullable 或空对象模式。
反应式是不是银弹?
不是。反应式适合 IO 密集、高并发、事件驱动场景;CRUD 项目用反应式只会增加复杂度。判题回调用反应式,后台 CRUD 用 Servlet 阻塞式,合理分工。
函数式里的"不可变"怎么保证?
字段用 final,对象用 record,集合用 List.of() / Collections.unmodifiableList(),方法不修改入参。
Predicate 和 Function 什么时候用?
Predicate 返回 boolean,做判断;Function 做转换;Consumer 做消费;Supplier 做生产。判题规则用 Predicate,代码转换用 Function。
错法对照 错法 问题 正解 stream.map(s -> { save(s); return s; })map 里产生副作用 副作用放 forEach 或独立步骤 Optional<Submission> fieldOptional 不能当字段 字段用 @Nullable,返回值用 Optional list.stream().forEach(System.out::println) 打日志打印是副作用,且顺序不确定 收集后统一输出,或用日志框架 用反应式包所有代码 复杂度爆炸,调试困难 只在 IO 边界用反应式,业务内部保持命令式 多层嵌套 if (x != null) 空指针隐患 + 可读性差 Optional 链式或提前 return
卡点急救
Stream 写复杂了 :拆成多个中间变量,每步只做一件事;或者用方法引用替代 lambda。
不知道 Optional 怎么终止链 :用 orElse(有默认值)、orElseThrow(必须存在)、orElseGet(懒加载默认值)。
反应式代码调试难 :加 .log() 看信号流;用 StepVerifier 写测试;关键路径加 doOnNext/doOnError。
面试常问
Stream 和 for 循环怎么选?
答:简单转换、过滤、聚合用 Stream,可读性好且容易并行;复杂业务流转、需要中途修改状态、需要精确控制异常用 for。判题平台统计用 Stream,提交入库用 for/命令式。
Optional 的最佳实践?
答:只用于返回值,别用于字段和参数;orElse 和 orElseGet 区别(前者立即执行,后者懒加载);orElseThrow 用于必须存在的情况,别用 get()。
反应式编程的核心思想?
答:非阻塞 + 事件驱动 + 背压。IO 操作不占用线程等待,数据来了再处理;消费者告诉生产者"我还能收多少",防止压垮。判题回调、WebSocket 推送适合反应式;CRUD 不适合。
自测三问
把"统计某课程 AC 率"写成 Stream 版本,并指出每一步的输入输出。
为什么 Optional 不能当字段?如果要表达"用户可能没有昵称",应该怎么做?
判题回调用反应式,后台 CRUD 用阻塞式,这样分工的依据是什么?
小结一句话 命令式管业务流转,函数式管数据转换,反应式管高并发 IO;范式是工具,不是信仰。
延伸
《Java 8 实战》Raoul-Gabriel Urma 等,Stream 与 Optional 章节
《函数式编程思维》Neal Ford
Project Reactor 官方文档
下一课:设计模式——GoF 模式在判题平台的落地。