函数式编程思想与函数式接口 | JavaSE
函数式编程思想与函数式接口
一、学习目标
完成本章后,你应该能够:
- 能够解释什么是函数式编程思想,以及它与传统“命令式写法”的关注点差异。
- 能够理解 Java 为什么在面向对象体系中引入 Lambda 与函数式编程能力。
- 能够准确判断一个接口是不是函数式接口。
- 能够解释函数式接口为什么可以作为 Lambda 表达式的目标类型。
- 能够正确使用
@FunctionalInterface声明和约束自定义函数式接口。 - 能够判断默认方法、静态方法等是否会破坏函数式接口的成立条件。
- 能够理解“把行为作为参数传递”这一思想,为后续 Lambda、方法引用和 Stream 建立知识基础。
二、核心知识
2.1 为什么这一章要放在 Lambda 前面
在正式学习 Lambda 语法之前,我们首先要回答一个更重要的问题:
Java 为什么需要 Lambda?
如果不知道这个问题,Lambda 很容易被学成一种:
“匿名内部类的简写语法”
然后记住:
() -> {}
却不知道它背后真正改变了什么。
Lambda 背后的核心思想之一是:
把一段行为当成可以传递的数据来使用。
以前我们经常传:
int
String
Student
List<Student>
以后我们还会传:
“怎么比较”
“怎么筛选”
“怎么处理”
“满足什么条件”
“拿到数据之后做什么”
也就是说:
不仅数据可以作为参数,行为也可以作为参数。
这正是后面:
- Lambda
- 方法引用
- Stream
- Comparator
- forEach
- Predicate
- Consumer
能够串联起来的重要思想基础。
2.2 什么是函数式编程思想
项目原始教学资料对函数式编程的入门描述是:
使用 Lambda 函数替代某些匿名内部类对象,使程序代码更加简洁、可读性更好。
并使用数学函数进行了类比。
数学中:
f(x) = 2x + 1
我们关心的是:
输入 x
↓
按照规则计算
↓
得到结果
例如:
x = 3
2 × 3 + 1 = 7
重点是:
给什么数据,按照什么规则处理,得到什么结果。
在 Java 中,函数式编程同样更加关注:
“我要对数据做什么?”
而不是首先关注:
“我要创建哪个实现类对象?”
2.3 从“对象”到“行为”
考虑一个简单需求:
有两个整数,需要根据不同规则计算结果。
传统面向对象思路可以定义接口:
interface Calculator {
int calculate(int a, int b);
}
然后针对不同算法创建不同实现类:
class AddCalculator implements Calculator {
@Override
public int calculate(int a, int b) {
return a + b;
}
}
class SubCalculator implements Calculator {
@Override
public int calculate(int a, int b) {
return a - b;
}
}
使用:
Calculator calculator = new AddCalculator();
int result = calculator.calculate(10, 20);
这种方式没有错误。
它体现的是经典面向对象思想:
行为
↓
封装到类
↓
创建对象
↓
通过对象调用行为
但如果:
- 实现逻辑非常短;
- 对象只使用一次;
- 我真正关心的是计算规则;
那么为了这一小段行为创建完整实现类,就显得比较繁琐。
2.4 匿名内部类解决了一部分问题
前面学习匿名内部类时,我们可以写:
Calculator calculator = new Calculator() {
@Override
public int calculate(int a, int b) {
return a + b;
}
};
不再需要额外创建:
AddCalculator
这样的类。
但是仍然存在大量模板代码:
new Calculator() {
@Override
public int calculate(int a, int b) {
}
}
真正属于业务行为的其实只有:
return a + b;
可以粗略观察:
new Calculator() {
@Override
public int calculate(int a, int b) {
return a + b;
}
}
其中真正想表达的是:
两个参数
↓
执行 a + b
↓
返回结果
因此 Java 8 引入 Lambda 表达式后,可以更加直接地表达这个行为。
具体 Lambda 语法将在下一章正式学习。
本章目前只需要理解:
Lambda 的价值不是单纯“少写几个字符”,而是让程序可以更加直接地描述行为。
2.5 命令式思维与函数式思维
这里可以先建立一个简单的对比。
假设有:
List<Integer> numbers =
List.of(1, 2, 3, 4, 5, 6);
需求:
找出所有偶数。
传统循环:
for (Integer number : numbers) {
if (number % 2 == 0) {
System.out.println(number);
}
}
我们的思考过程是:
拿到集合
↓
创建循环
↓
逐个取得元素
↓
判断是否为偶数
↓
满足条件就输出
这种思维比较关注:
程序一步一步应该怎么执行。
可以称为一种典型的命令式(Imperative)表达方式。
而后面学习 Stream 后可能看到:
numbers.stream()
.filter(number -> number % 2 == 0)
.forEach(System.out::println);
此时更加关注:
筛选偶数
↓
输出结果
也就是:
描述“我要什么”,而不是把每一步循环控制都手工写出来。
现在暂时不要求掌握这段 Stream 代码。
这里只需要建立一个认识:
Java 的函数式能力会让我们逐渐从“手工控制每一步”,转向“声明数据应该怎样被处理”。
2.6 Java 是不是纯函数式编程语言
不是。
Java 的核心仍然建立在:
- 类
- 对象
- 接口
- 封装
- 继承
- 多态
这些面向对象机制之上。
Java 8 之后加入:
- Lambda 表达式
- 方法引用
java.util.function- Stream API
使 Java 获得了更强的函数式编程能力。
因此更准确的理解是:
Java
├── 面向对象编程
│
├── 命令式编程
│
└── 支持函数式编程风格
它不是要求:
“以后不要面向对象,全都写 Lambda。”
而是让开发者可以根据问题选择更加合适的表达方式。
三、函数式接口
3.1 为什么 Lambda 需要接口
Java 是一门强类型语言。
假设未来看到:
(a, b) -> a + b
Java 必须知道:
a是什么类型?b是什么类型?- 返回什么类型?
- 这段行为到底代表什么?
因此 Lambda 不能凭空存在。
它需要一个明确的类型作为约束。
这个重要角色就是:
函数式接口(Functional Interface)。
3.2 什么是函数式接口
项目原始教学资料给出的定义是:
有且仅有一个抽象方法的接口,就是函数式接口。
例如:
interface Swimming {
void swim();
}
只有一个抽象方法:
void swim();
因此:
Swimming
是函数式接口。
3.3 两个抽象方法就不是函数式接口
例如:
interface Player {
void play();
void stop();
}
这里存在:
play()
stop()
两个不同的抽象方法。
因此:
Player
不是函数式接口。
为什么?
因为如果以后写:
一段 Lambda 行为
编译器无法判断:
这段行为究竟是在实现
play(),还是在实现stop()?
所以函数式接口必须能够形成一个明确的:
单一函数契约。
3.4 什么叫“函数契约”
例如:
interface Calculator {
int calculate(int a, int b);
}
它实际上规定了一种行为:
输入:
两个 int
行为:
进行某种计算
输出:
一个 int
至于具体:
a + b
a - b
a * b
接口并没有规定。
这就是:
Calculator
↓
描述行为的规格
↓
int + int → int
具体实现可以不同。
因此函数式接口可以理解成:
描述一种行为形状的类型。
3.5 函数式接口与普通接口是什么关系
函数式接口并不是一种全新的 Java 类型。
它本质仍然是:
interface
例如:
interface Calculator {
int calculate(int a, int b);
}
完全可以:
class MyCalculator implements Calculator {
@Override
public int calculate(int a, int b) {
return a + b;
}
}
也可以使用匿名内部类:
Calculator calculator =
new Calculator() {
@Override
public int calculate(int a, int b) {
return a + b;
}
};
后面还可以使用 Lambda。
所以:
函数式接口
↓
首先仍然是接口
↓
只是满足“单一抽象行为契约”
↓
因此能够作为 Lambda 的目标类型
四、@FunctionalInterface
4.1 基本写法
Java 提供:
@FunctionalInterface
用于明确声明:
这个接口设计上应该是函数式接口。
例如:
@FunctionalInterface
interface Calculator {
int calculate(int a, int b);
}
4.2 @FunctionalInterface 的作用
假设:
@FunctionalInterface
interface Calculator {
int calculate(int a, int b);
}
现在误加一个新的抽象方法:
@FunctionalInterface
interface Calculator {
int calculate(int a, int b);
int max(int a, int b);
}
接口已经拥有两个独立抽象方法。
编译器会直接报错。
因此这个注解的重要价值是:
让编译器帮助我们保护接口的设计意图。
也就是说:
程序员:
这个接口必须保持函数式接口
↓
@FunctionalInterface
↓
编译器:
如果以后有人破坏这个条件,我立即报错
4.3 不写 @FunctionalInterface 还是函数式接口吗
是。
例如:
interface Calculator {
int calculate(int a, int b);
}
即使没有:
@FunctionalInterface
只要它满足函数式接口的条件,它依然是函数式接口。
所以:
@FunctionalInterface
≠
“把普通接口变成函数式接口”
而应该理解成:
接口本身满足条件
↓
它就是函数式接口
@FunctionalInterface
↓
额外让编译器检查并表达设计意图
因此在自己明确设计函数式接口时,通常推荐写:
@FunctionalInterface
4.4 默认方法会不会破坏函数式接口
不会。
例如:
@FunctionalInterface
interface Calculator {
int calculate(int a, int b);
default void printInfo() {
System.out.println("Calculator");
}
}
这里:
calculate()
是抽象方法。
而:
printInfo()
已经拥有方法体。
因此它不是另一个抽象方法。
这个接口仍然可以保持函数式接口性质。
4.5 静态方法会不会破坏函数式接口
也不会。
例如:
@FunctionalInterface
interface Calculator {
int calculate(int a, int b);
static void info() {
System.out.println("Calculator");
}
}
这里仍然只有一个需要实现的抽象行为:
calculate()
所以仍然可以是函数式接口。
4.6 私有方法会不会破坏函数式接口
不会。
例如:
@FunctionalInterface
interface Calculator {
int calculate(int a, int b);
private void log() {
System.out.println("log");
}
}
私有接口方法有具体实现,也不属于需要实现类完成的那个抽象函数契约。
4.7 因此真正要数的是什么
不要机械地:
数接口里面一共有几个方法。
应该判断:
这个接口到底存在几个彼此独立、需要实现的抽象方法契约。
例如:
@FunctionalInterface
interface Demo {
void test();
default void method1() {
}
static void method2() {
}
private void method3() {
}
}
虽然肉眼看到了四个方法:
test
method1
method2
method3
但是只有:
void test();
是抽象方法。
所以它仍然可以作为函数式接口。
五、函数式接口与 Lambda 的关系
5.1 Lambda 依赖函数式接口
可以建立这样一条非常重要的关系:
函数式接口
↓
定义“行为应该长什么样”
Lambda
↓
提供“这个行为具体怎么做”
例如接口:
@FunctionalInterface
interface Calculator {
int calculate(int a, int b);
}
行为规格:
两个 int
↓
某种计算
↓
一个 int
后面 Lambda 可以为这个行为提供不同实现:
加法
减法
乘法
除法
接口负责:
定义能力。
Lambda 负责:
提供行为。
5.2 为什么必须只有一个抽象方法
假设:
interface Calculator {
int calculate(int a, int b);
boolean check(int number);
}
这时接口里面存在两个完全不同的行为契约:
(int, int) → int
int → boolean
如果只给一段 Lambda:
???
根本无法唯一确定它应该实现哪一个。
所以函数式接口要求最终能够确定:
一个唯一的函数方法(Functional Method)。
六、实践应用
6.1 Runnable
我们以后学习多线程会遇到:
Runnable
它的核心行为是:
void run();
可以理解为:
不接收参数
↓
执行一个任务
↓
不返回结果
这是一种非常典型的:
“执行型行为”。
6.2 Comparator
前面集合框架已经学习:
Comparator<T>
其核心比较行为是:
int compare(T o1, T o2);
它表达的是:
接收两个对象
↓
执行比较规则
↓
返回比较结果
所以 Comparator 的真正价值不仅是“排序接口”。
还可以从函数式角度理解:
它封装的是一条比较行为。
这也是为什么 Comparator 后面可以非常自然地与 Lambda 配合。
具体实战将在 06-03 展开。
6.3 行为参数化
考虑:
public static void calculate(
int a,
int b,
Calculator calculator
) {
int result =
calculator.calculate(a, b);
System.out.println(result);
}
这里:
a
b
是普通数据。
而:
calculator
代表:
怎么处理这两个数据。
因此调用者不仅可以向方法传:
数据
还可以传:
处理数据的策略
整个结构变成:
数据
+
行为
↓
通用方法
↓
得到结果
这就是非常重要的:
行为参数化(Behavior Parameterization)思想。
6.4 为什么行为参数化很重要
如果没有行为参数化:
add()
subtract()
multiply()
divide()
可能分别写多个高度相似的方法。
但如果把变化的部分:
“到底怎么算”
抽象成行为参数,那么公共流程可以复用。
例如:
固定部分:
拿数据
执行计算
输出结果
变化部分:
具体计算规则
这和以前学习过的:
- 多态
- 接口
- 策略思想
其实并不是割裂的。
函数式编程是在 Java 原有接口、多态体系上,提供了一种更加轻量的行为表达方式。
七、Java 标准函数式接口初识
Java 标准库中有:
java.util.function
包。
其中定义了一批常用函数式接口。
现在不要求全部掌握,只需要先建立整体认识。
7.1 Consumer
Consumer<T>
可以理解为:
输入 T
↓
执行操作
↓
没有返回值
概念模型:
T → void
典型含义:
消费一个数据。
例如:
打印
保存
处理
7.2 Supplier
Supplier<T>
概念模型:
无参数 → T
表示:
不接收数据,但提供一个结果。
例如:
生成对象
提供数据
产生随机值
7.3 Predicate
Predicate<T>
概念模型:
T → boolean
表示:
对数据进行条件判断。
例如:
是否成年
是否及格
是否为空
是否满足筛选条件
7.4 Function
Function<T, R>
概念模型:
T → R
表示:
接收一种数据,经过处理转换成另一种结果。
例如:
Student → String
商品 → 价格
字符串 → 长度
7.5 四种基本思想
可以先建立这张图:
| 类型 | 输入 | 输出 | 核心含义 |
| --------------- | ---- | ------- | -------- |
| Consumer<T> | T | void | 消费 |
| Supplier<T> | 无 | T | 提供 |
| Predicate<T> | T | boolean | 判断 |
| Function<T,R> | T | R | 转换 |
以后学习 Stream 时,你会频繁看到这些函数形状。
现在最重要的是理解:
函数式接口实际上是在给“行为”建立类型。
八、常见问题
8.1 函数式接口是不是一种新的 interface 语法
不是。
仍然使用:
interface
只是这个接口满足函数式接口的语义条件。
8.2 函数式接口是不是必须写 @FunctionalInterface
不是。
不写注解,只要满足条件,仍然可以是函数式接口。
但是如果接口设计目的明确就是函数式接口,推荐使用:
@FunctionalInterface
让编译器帮助检查。
8.3 @FunctionalInterface 能不能把两个抽象方法的接口变成函数式接口
不能。
例如:
@FunctionalInterface
interface Test {
void a();
void b();
}
不会因为写了注解就合法。
恰恰相反:
注解会让编译器发现这个接口违反函数式接口约束。
8.4 函数式接口只能有一个方法吗
错误。
更准确应该说:
函数式接口要求具有一个函数方法对应的抽象行为契约。
它依然可以存在:
default
static
private
等有具体实现的方法。
所以不要记成:
“接口里只能出现一个方法。”
8.5 函数式编程是不是就是 Lambda
不是。
Lambda 是:
Java 用于表达函数式行为的重要语法工具。
函数式编程是更上层的:
编程思想和代码组织方式。
类似:
面向对象编程
≠
class 关键字
同样:
函数式编程
≠
Lambda 语法本身
8.6 学了函数式编程以后是不是就不需要面向对象
不是。
Java 中:
接口
多态
泛型
Lambda
函数式接口
Stream
往往是协同工作的。
例如:
Predicate<Student>
本身仍然是:
泛型
+
接口
+
函数式接口
Lambda 并没有摧毁 Java 的面向对象类型系统。
它是在这个类型系统之上提供更加简洁的行为表达能力。
九、练习与验收
9.1 知识问答
- 什么是函数式编程思想?
- 项目原始课程中认为函数式编程主要解决了什么问题?
- 函数式编程与 Lambda 是不是同一个概念?
- 什么是函数式接口?
- 为什么函数式接口只能具有一个函数方法对应的抽象行为契约?
@FunctionalInterface有什么作用?- 不写
@FunctionalInterface,接口还能成为函数式接口吗? default方法会破坏函数式接口吗?static方法会破坏函数式接口吗?- Lambda 与函数式接口之间是什么关系?
- 什么叫行为参数化?
- 为什么 Comparator 可以从“比较规则”这个角度理解为行为?
Consumer<T>表示什么类型的行为?Supplier<T>表示什么类型的行为?Predicate<T>表示什么类型的行为?Function<T,R>表示什么类型的行为?
9.2 代码阅读
判断下面哪些接口可以作为函数式接口。
接口 A
interface A {
void run();
}
接口 B
interface B {
void run();
void stop();
}
接口 C
interface C {
void run();
default void stop() {
System.out.println("stop");
}
}
接口 D
interface D {
int calculate(int a, int b);
static void info() {
System.out.println("D");
}
}
要求逐个说明理由,不能只回答:
是 / 不是
9.3 手写代码
任务一
定义函数式接口:
Calculator
要求抽象方法:
int calculate(int a, int b);
并使用:
@FunctionalInterface
约束。
本题暂时不要使用 Lambda。
请使用普通实现类实现加法。
任务二
定义:
Printer
表示:
String → void
自己设计合理的抽象方法。
任务三
定义:
NumberChecker
表示:
int → boolean
自己设计合理的抽象方法。
任务四
定义一个函数式接口,表示:
无输入
→
产生一个 String
要求自行设计:
- 接口名
- 方法名
- 返回值类型
9.4 Debug
下面代码存在什么问题?
@FunctionalInterface
interface Fly {
void fly();
void stop();
}
回答:
- 编译器为什么会报错?
@FunctionalInterface是错误的根源吗?- 删除注解以后接口还能正常定义吗?
- 删除注解以后它还是函数式接口吗?
分析:
@FunctionalInterface
interface Test {
void run();
default void print() {
System.out.println("Hello");
}
}
回答:
- 该接口是否合法?
- 有几个方法?
- 有几个抽象方法?
- 是否仍然属于函数式接口?
9.5 综合训练
设计一个简单的“数据处理器”。
要求定义:
@FunctionalInterface
interface IntProcessor {
int process(int number);
}
然后设计一个通用方法:
public static int process(
int number,
IntProcessor processor
)
暂时不要使用 Lambda。
分别创建普通实现类完成:
数字 × 2
数字平方
数字 + 100
最后回答:
number参数代表什么?processor参数代表什么?- 什么部分是固定流程?
- 什么部分是变化行为?
- 为什么可以说这里已经存在“行为参数化”的思想?
- 下一章引入 Lambda 后,最有可能被简化的是哪部分代码?
9.6 本章验收
关闭资料后,能够完整解释下面这条知识链:
传统实现类
↓
匿名内部类
↓
大量模板代码
↓
希望直接表达行为
↓
函数式编程思想
↓
行为参数化
↓
函数式接口定义行为契约
↓
Lambda 提供行为实现
↓
后续进入方法引用与 Stream
同时能够闭卷回答:
什么是函数式接口?
@FunctionalInterface 到底干什么?
为什么不写注解也可能是函数式接口?
default/static 方法为什么不会直接破坏函数式接口?
为什么 Lambda 需要函数式接口?
Consumer / Supplier / Predicate / Function
分别代表什么行为形状?
能够独立判断以下接口是否为函数式接口:
interface A {
void run();
}
interface B {
void run();
void stop();
}
interface C {
void run();
default void stop() {
}
}
并能够说明原因,而不仅是给出结论,本章才算真正掌握。