如果你长期写面向对象的代码,第一次接触函数式编程(FP)可能会觉得”别扭”。

但理解之后你会发现,它不是要取代面向对象,而是给了你另一种处理复杂问题的思维方式。

函数式编程的核心思想

函数式编程有三个核心概念,理解了它们就抓住了 FP 的精髓。

1. 纯函数(Pure Function)

纯函数满足两个条件:

  1. 同样的输入,永远得到同样的输出(确定性)
  2. 不产生任何副作用(不修改外部状态)
1
2
3
4
5
6
7
8
9
10
// 纯函数:输入相同,输出必然相同
int add(int a, int b) {
return a + b;
}

// 非纯函数:依赖并修改了外部状态
int counter = 0;
int increment() {
return ++counter;
}

为什么纯函数好?因为:

  • 容易测试:不需要 mock 一堆状态
  • 容易推理:看输入输出就能理解,不用追踪外部状态
  • 天生线程安全:没有共享可变状态,就没有并发问题

2. 不可变数据(Immutability)

函数式编程倾向于使用不可变数据:对象一旦创建,就永不改变。要”修改”,就创建一个新的。

1
2
3
4
5
// 可变:直接改原对象
list.add("new");

// 不可变:返回新列表,原列表不变
List<String> newList = Stream.concat(list.stream(), Stream.of("new")).toList();

好处:不可变数据让程序的状态变化变得可预测,特别适合并发场景。

代价:频繁创建新对象有性能开销,所以需要配合”结构共享”(如持久化数据结构)来优化。

3. 高阶函数(Higher-order Function)

高阶函数:把函数当作参数传入,或把函数作为返回值返回。

1
2
3
4
List<Integer> result = list.stream()
.filter(x -> x > 0) // filter 接收一个函数
.map(x -> x * 2) // map 也接收一个函数
.toList();

filter、map 都是高阶函数——它们接收”函数”作为参数,让你把”怎么做”抽象出来。

函数式 vs 面向对象

维度 面向对象 函数式
核心抽象 对象(状态 + 行为) 函数(输入 → 输出)
状态管理 封装可变状态 尽量不可变
关注点 “谁来做” “做什么”
优势 建模复杂实体 处理数据流、并发、易测试

它们不是对立的,现代语言都是两者兼有。

Java 8 引入 Lambda、Stream、Optional,就是给面向对象语言加上了函数式能力。Go 虽然没有完整的 FP 支持,但”函数是一等公民”的理念同样适用。

函数式编程在实际中的价值

1. 让代码更声明式

命令式(告诉怎么做):

1
2
3
4
5
6
List<Integer> result = new ArrayList<>();
for (int x : list) {
if (x > 0) {
result.add(x * 2);
}
}

声明式(告诉做什么):

1
2
3
4
List<Integer> result = list.stream()
.filter(x -> x > 0)
.map(x -> x * 2)
.toList();

声明式更接近”业务意图”,读起来像在描述需求,而不是描述实现细节。

2. 减少并发 bug

很多并发 bug 的根源是”多个线程共享可变状态”。函数式鼓励不可变数据,从源头减少了这类问题。

3. 更容易测试和推理

纯函数没有隐藏依赖,测试就是”给输入、断言输出”,简单直接。

一个需要警惕的地方

函数式不是银弹,也有代价:

  1. 性能:不可变对象频繁创建,可能有 GC 压力
  2. 学习曲线:对习惯面向对象的人,思维转变需要时间
  3. 过度使用:一条简单的 for 循环硬写成复杂的 Stream 链,反而难读

我的看法是:用函数式的”思想”(纯函数、不可变、声明式),但不教条。哪里清晰用哪里。

写在最后

函数式编程给我最大的启发,不是具体的语法,而是一个价值观:

让数据流”单向”流动,让状态变化”显式”可控。

这个价值观,无论你写 Java、Go,还是未来学 Rust、Scala,都是通用的。

下一篇聊一个更贴近日常工作的话题——单元测试和 TDD。