Jakob Jenkov 2023-02-13
“Java 函数式编程”指的是在 Java 中使用函数式编程范式。历史上,Java 并不容易支持函数式编程,甚至有些函数式编程的特性在 Java 中根本无法实现。直到 Java 8,Oracle 才开始努力让函数式编程变得更简单,这一努力在一定程度上取得了成功。
在本篇 Java 函数式编程教程中,我将介绍函数式编程的基础知识,并说明其中哪些部分可以在 Java 中实现。
函数式编程基础
函数式编程包含以下核心概念:
- 函数是一等公民(First-class objects)
- 纯函数(Pure functions)
- 高阶函数(Higher-order functions)
而纯函数式编程还遵循一组规则:
- 无状态(No state)
- 无副作用(No side effects)
- 不可变变量(Immutable variables)
- 优先使用递归而非循环(Favour recursion over looping)
这些概念和规则将在本教程后续部分逐一解释。
即使你不能始终严格遵守所有规则,依然可以从函数式编程的思想中获益。正如你将看到的那样,函数式编程并非适用于所有问题。特别是“无副作用”的理念,使得诸如写入数据库这类操作(属于副作用)变得困难。你需要学会判断哪些问题适合用函数式编程来解决,哪些不适合。
函数是一等公民
在函数式编程范式中,函数是语言中的一等公民。这意味着你可以像创建 String、Map 或其他对象的实例一样,创建一个“函数实例”,并用变量引用它。函数也可以作为参数传递给其他函数。
在 Java 中,方法并不是一等公民。最接近的实现是 Java Lambda 表达式。本教程不会详细讲解 Java Lambda 表达式,因为我已在 Java Lambda 表达式教程(含文字和视频)中做了详细介绍。
纯函数(Pure Functions)
如果一个函数满足以下两个条件,它就是一个纯函数:
- 函数的执行没有副作用;
- 函数的返回值仅依赖于传入的参数。
下面是一个 Java 中纯函数(方法)的例子:
public class ObjectWithPureFunction {
public int sum(int a, int b) {
return a + b;
}
}
注意,sum() 方法的返回值只依赖于输入参数,并且没有任何副作用(即不会修改函数外部的任何状态)。
相反,下面是一个非纯函数的例子:
public class ObjectWithNonPureFunction {
private int value = 0;
public int add(int nextValue) {
this.value += nextValue;
return this.value;
}
}
注意,add() 方法使用了成员变量来计算返回值,并且修改了 value 成员变量的状态,因此具有副作用。
高阶函数(Higher Order Functions)
如果一个函数满足以下任一条件,它就是高阶函数:
- 函数接受一个或多个函数作为参数;
- 函数返回另一个函数作为结果。
在 Java 中,我们能实现的最接近高阶函数的形式是:方法接受一个或多个 Lambda 表达式作为参数,并返回另一个 Lambda 表达式。
下面是一个 Java 中高阶函数的例子:
public class HigherOrderFunctionClass {
public <T> IFactory<T> createFactory(IProducer<T> producer, IConfigurator<T> configurator) {
return () -> {
T instance = producer.produce();
configurator.configure(instance);
return instance;
};
}
}
注意,createFactory() 方法返回了一个 Lambda 表达式,这满足高阶函数的第一个条件。
此外,该方法接收的两个参数 producer 和 configurator 都是接口的实现(IProducer 和 IConfigurator)。由于 Java 的 Lambda 表达式必须实现一个函数式接口(functional interface),因此只要这些接口符合要求,就可以用 Lambda 表达式来实现。
假设这些接口如下所示:
public interface IFactory<T> {
T create();
}
public interface IProducer<T> {
T produce();
}
public interface IConfigurator<T> {
void configure(T t);
}
可以看到,这三个接口都是函数式接口(只有一个抽象方法),因此可以被 Lambda 表达式实现。所以,createFactory() 是一个高阶函数。
无状态(No State)
如前所述,函数式编程范式的一个规则是无状态。这里的“无状态”通常指函数不依赖外部状态。函数内部可以有局部变量用于临时存储状态,但不能引用所属类或对象的成员变量。
下面是一个不使用外部状态的函数示例:
public class Calculator {
public int sum(int a, int b) {
return a + b;
}
}
而下面这个函数则使用了外部状态,违反了“无状态”规则:
public class Calculator {
private int initVal = 5;
public int sum(int a) {
return initVal + a;
}
}
无副作用(No Side Effects)
函数式编程的另一条规则是无副作用,即函数不能改变其外部的任何状态。这种对外部状态的修改被称为副作用。
“外部状态”既包括函数所属类或对象的成员变量,也包括函数参数内部的成员变量,以及文件系统、数据库等外部系统的状态。
不可变变量(Immutable Variables)
函数式编程的第三条规则是使用不可变变量。不可变变量有助于避免副作用,使程序更易于推理和测试。
优先使用递归而非循环(Favour Recursion Over Looping)
函数式编程的第四条规则是优先使用递归而不是传统的循环结构。递归通过函数调用来实现重复逻辑,使代码更具函数式风格。
不过,在 Java 中,另一种替代循环的方式是使用 Java Stream API,该 API 本身也受到函数式编程的启发。
函数式接口(Functional Interfaces)
Java 中的函数式接口是指只包含一个抽象方法的接口。所谓“抽象方法”,指的是没有实现的方法。一个接口可以包含多个带有实现的方法(如 default 方法或 static 方法),但只要只有一个未实现的方法,它就被视为函数式接口。
例如,以下是一个函数式接口:
public interface MyInterface {
public void run();
}
下面这个接口虽然包含 default 和 static 方法,但仍然是函数式接口:
public interface MyInterface2 {
public void run();
public default void doIt() {
System.out.println("doing it");
}
public static void doItStatically() {
System.out.println("doing it statically");
}
}
注意,只有 run() 是抽象方法,其余都有实现。因此它仍可被 Lambda 表达式实现。
但如果接口中有多个未实现的方法,它就不再是函数式接口,也就不能用 Lambda 表达式来实现。