函数式编程范式:纯函数、不可变数据与高阶函数
如果你长期写面向对象的代码,第一次接触函数式编程(FP)可能会觉得”别扭”。
但理解之后你会发现,它不是要取代面向对象,而是给了你另一种处理复杂问题的思维方式。
函数式编程的核心思想
函数式编程有三个核心概念,理解了它们就抓住了 FP 的精髓。
1. 纯函数(Pure Function)
纯函数满足两个条件:
- 同样的输入,永远得到同样的输出(确定性)
- 不产生任何副作用(不修改外部状态)
1 | // 纯函数:输入相同,输出必然相同 |
为什么纯函数好?因为:
- 容易测试:不需要 mock 一堆状态
- 容易推理:看输入输出就能理解,不用追踪外部状态
- 天生线程安全:没有共享可变状态,就没有并发问题
2. 不可变数据(Immutability)
函数式编程倾向于使用不可变数据:对象一旦创建,就永不改变。要”修改”,就创建一个新的。
1 | // 可变:直接改原对象 |
好处:不可变数据让程序的状态变化变得可预测,特别适合并发场景。
代价:频繁创建新对象有性能开销,所以需要配合”结构共享”(如持久化数据结构)来优化。
3. 高阶函数(Higher-order Function)
高阶函数:把函数当作参数传入,或把函数作为返回值返回。
1 | List<Integer> result = list.stream() |
filter、map 都是高阶函数——它们接收”函数”作为参数,让你把”怎么做”抽象出来。
函数式 vs 面向对象
| 维度 | 面向对象 | 函数式 |
|---|---|---|
| 核心抽象 | 对象(状态 + 行为) | 函数(输入 → 输出) |
| 状态管理 | 封装可变状态 | 尽量不可变 |
| 关注点 | “谁来做” | “做什么” |
| 优势 | 建模复杂实体 | 处理数据流、并发、易测试 |
它们不是对立的,现代语言都是两者兼有。
Java 8 引入 Lambda、Stream、Optional,就是给面向对象语言加上了函数式能力。Go 虽然没有完整的 FP 支持,但”函数是一等公民”的理念同样适用。
函数式编程在实际中的价值
1. 让代码更声明式
命令式(告诉怎么做):
1 | List<Integer> result = new ArrayList<>(); |
声明式(告诉做什么):
1 | List<Integer> result = list.stream() |
声明式更接近”业务意图”,读起来像在描述需求,而不是描述实现细节。
2. 减少并发 bug
很多并发 bug 的根源是”多个线程共享可变状态”。函数式鼓励不可变数据,从源头减少了这类问题。
3. 更容易测试和推理
纯函数没有隐藏依赖,测试就是”给输入、断言输出”,简单直接。
一个需要警惕的地方
函数式不是银弹,也有代价:
- 性能:不可变对象频繁创建,可能有 GC 压力
- 学习曲线:对习惯面向对象的人,思维转变需要时间
- 过度使用:一条简单的 for 循环硬写成复杂的 Stream 链,反而难读
我的看法是:用函数式的”思想”(纯函数、不可变、声明式),但不教条。哪里清晰用哪里。
写在最后
函数式编程给我最大的启发,不是具体的语法,而是一个价值观:
让数据流”单向”流动,让状态变化”显式”可控。
这个价值观,无论你写 Java、Go,还是未来学 Rust、Scala,都是通用的。
下一篇聊一个更贴近日常工作的话题——单元测试和 TDD。
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 高晓庄 札记!
