函数类型与高阶函数对函数式编程支持程度高低的一个重要特征是函数是否作为编程语言的一等公民出现,也就是编程语言是否有内置的结构来表示函数。作为面向对象的编程语言,Java 中使用接口来表示函数。直到 Java 8,Java 才提供了内置标准 API 来表示函数,也就是 java.util.function 包。Function<T, R> 表示接受一个参数的函数,输入类型为 T,输出类型为 R。Function 接口只包含一个抽象方法 R apply(T t),也就是在类型为 T 的输入 t 上应用该函数,得到类型为 R 的输出。除了接受一个参数的 Function 之外,还有接受两个参数的接口 BiFunction<T, U, R>,T 和 U 分别是两个参数的类型,R 是输出类型。BiFunction 接口的抽象方法为 R apply(T t, U u)。超过 2 个参数的函数在 Java 标准库中并没有定义。如果函数需要 3 个或更多的参数,可以使用第三方库,如 Vavr 中的 Function0 到 Function8。
除了 Function 和 BiFunction 之外,Java 标准库还提供了几种特殊类型的函数:
- Consumer<T>:接受一个输入,没有输出。抽象方法为 void accept(T t)。
- Supplier<T>:没有输入,一个输出。抽象方法为 T get()。
- Predicate<T>:接受一个输入,输出为 boolean 类型。抽象方法为 boolean test(T t)。
- UnaryOperator<T>:接受一个输入,输出的类型与输入相同,相当于 Function<T, T>。
- BinaryOperator<T>:接受两个类型相同的输入,输出的类型与输入相同,相当于 BiFunction<T,T,T>。
- BiPredicate<T, U>:接受两个输入,输出为 boolean 类型。抽象方法为 boolean test(T t, U u)。
在本系列的第一篇文章中介绍 λ 演算时,提到了高阶函数的概念。λ 项在定义时就支持以 λ 项进行抽象和应用。具体到实际的函数来说,高阶函数以其他函数作为输入,或产生其他函数作为输出。高阶函数使得函数的组合成为可能,更有利于函数的复用。熟悉面向对象的读者对于对象的组合应该不陌生。在划分对象的职责时,组合被认为是优于继承的一种方式。在使用对象组合时,每个对象所对应的职责单一。多个对象通过组合的方式来完成复杂的行为。函数的组合类似对象的组合。上一节中提到的 reduce 就是一个高阶函数的示例,其参数 updateValue 也是一个函数。通过组合,reduce 把一部分逻辑代理给了作为其输入的函数 updateValue。不同的函数的嵌套层次可以很多,完成复杂的组合。
在 Java 中,可以使用函数类型来定义高阶函数。上述函数接口都可以作为方法的参数和返回值。Java 标准 API 已经大量使用了这样的方式。比如 Iterable 的 forEach 方法就接受一个 Consumer 类型的参数。
在清单 5 中,notEqual 返回值是一个 Predicate 对象,并使用在 Stream 的 filter 方法中。代码运行的输出结果为 2 和 3。
清单 5. 高阶函数示例1
2
3
4
5
6
7
8
9
10
11
12
| public class HighOrderFunctions {
private static <T> Predicate<T> notEqual(T t) {
return (v) -> !Objects.equals(v, t);
}
public static void main(String[] args) {
List.of(1, 2, 3)
.stream()
.filter(notEqual(1))
.forEach(System.out::println);
}
}
|
|