函数式编程思想与函数式接口 | 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 知识问答

  1. 什么是函数式编程思想?
  2. 项目原始课程中认为函数式编程主要解决了什么问题?
  3. 函数式编程与 Lambda 是不是同一个概念?
  4. 什么是函数式接口?
  5. 为什么函数式接口只能具有一个函数方法对应的抽象行为契约?
  6. @FunctionalInterface 有什么作用?
  7. 不写 @FunctionalInterface,接口还能成为函数式接口吗?
  8. default 方法会破坏函数式接口吗?
  9. static 方法会破坏函数式接口吗?
  10. Lambda 与函数式接口之间是什么关系?
  11. 什么叫行为参数化?
  12. 为什么 Comparator 可以从“比较规则”这个角度理解为行为?
  13. Consumer<T> 表示什么类型的行为?
  14. Supplier<T> 表示什么类型的行为?
  15. Predicate<T> 表示什么类型的行为?
  16. 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();
}

回答:

  1. 编译器为什么会报错?
  2. @FunctionalInterface 是错误的根源吗?
  3. 删除注解以后接口还能正常定义吗?
  4. 删除注解以后它还是函数式接口吗?

分析:

@FunctionalInterface
interface Test {

    void run();

    default void print() {
        System.out.println("Hello");
    }
}

回答:

  1. 该接口是否合法?
  2. 有几个方法?
  3. 有几个抽象方法?
  4. 是否仍然属于函数式接口?

9.5 综合训练

设计一个简单的“数据处理器”。

要求定义:

@FunctionalInterface
interface IntProcessor {

    int process(int number);
}

然后设计一个通用方法:

public static int process(
        int number,
        IntProcessor processor
)

暂时不要使用 Lambda。

分别创建普通实现类完成:

数字 × 2
数字平方
数字 + 100

最后回答:

  1. number 参数代表什么?
  2. processor 参数代表什么?
  3. 什么部分是固定流程?
  4. 什么部分是变化行为?
  5. 为什么可以说这里已经存在“行为参数化”的思想?
  6. 下一章引入 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() {
    }
}

并能够说明原因,而不仅是给出结论,本章才算真正掌握。