CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
软件设计与架构入门/课时/编程范式
RawBlame
课时目录12 节

软件设计与架构入门

  1. 01整洁代码
  2. 02设计原则
  3. 03面向对象
  4. 04编程范式
  5. 05设计模式
  6. 06架构原则
  7. 07架构风格
  8. 08架构模式
  9. 09领域驱动设计
  10. 10企业模式
  11. 11综合架构练习
  12. 12路线图对照复习

编程范式

函数式与结构化编程。

30 分钟

编程范式:函数式、结构化与反应式

这一课你将能做到

  1. 在判题平台代码里识别命令式、函数式、反应式三种风格,并知道各自适用场景
  2. 用 Java 17 的 Stream、Optional、Function、Predicate 写出无副作用的判题统计代码
  3. 用 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(), "判题完成");
}

主线程不等待,事件来了就处理,适合高并发判题回调。

讲清楚要点

  1. 命令式:告诉机器每一步怎么做,状态可变,适合业务流转和 IO。
  2. 函数式:数据流 + 不可变 + 无副作用,适合集合处理、规则组合、并发计算。
  3. 反应式:事件流 + 非阻塞 + 背压,适合高并发 IO、消息回调、WebSocket。
  4. Java 的函数式武器:Stream、Optional、Function、Predicate、Consumer、record、sealed。
  5. 副作用是万恶之源:map 里改全局变量、filter 里调 RPC,都是函数式反模式。
  6. 不要范式狂热:该命令式的地方(写库、改状态)别硬套 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

卡点急救

  1. Stream 写复杂了:拆成多个中间变量,每步只做一件事;或者用方法引用替代 lambda。
  2. 不知道 Optional 怎么终止链:用 orElse(有默认值)、orElseThrow(必须存在)、orElseGet(懒加载默认值)。
  3. 反应式代码调试难:加 .log() 看信号流;用 StepVerifier 写测试;关键路径加 doOnNext/doOnError。

面试常问

  • Stream 和 for 循环怎么选? 答:简单转换、过滤、聚合用 Stream,可读性好且容易并行;复杂业务流转、需要中途修改状态、需要精确控制异常用 for。判题平台统计用 Stream,提交入库用 for/命令式。
  • Optional 的最佳实践? 答:只用于返回值,别用于字段和参数;orElse 和 orElseGet 区别(前者立即执行,后者懒加载);orElseThrow 用于必须存在的情况,别用 get()。
  • 反应式编程的核心思想? 答:非阻塞 + 事件驱动 + 背压。IO 操作不占用线程等待,数据来了再处理;消费者告诉生产者"我还能收多少",防止压垮。判题回调、WebSocket 推送适合反应式;CRUD 不适合。

自测三问

  1. 把"统计某课程 AC 率"写成 Stream 版本,并指出每一步的输入输出。
  2. 为什么 Optional 不能当字段?如果要表达"用户可能没有昵称",应该怎么做?
  3. 判题回调用反应式,后台 CRUD 用阻塞式,这样分工的依据是什么?

小结一句话

命令式管业务流转,函数式管数据转换,反应式管高并发 IO;范式是工具,不是信仰。

延伸

  • 《Java 8 实战》Raoul-Gabriel Urma 等,Stream 与 Optional 章节
  • 《函数式编程思维》Neal Ford
  • Project Reactor 官方文档
  • 下一课:设计模式——GoF 模式在判题平台的落地。
加载本节练习…
← 上一课面向对象下一课 →设计模式