HIT-软件构造 | Java 函数式编程
函数式编程(Functional Programming)是编程范式的一种,常见的编程范式还有面向过程编程、面向对象编程(OOP)、面向数据编程(DOP)等。
程序设计语言发展的早期,人们将一段封装起来可供调用的代码块称为 “过程” 或 “函数” ;当函数绑定到对象上用于对象的操作时又被称为 “方法” ;有人始终认为编程语言中应该有真正数学意义上的函数,它应该是一种,对于确定的输入,有确定的输出、没有副作用的纯函数。
Lambda 表达式
函数式编程的特点之一是,它将函数视作变量,可以作为函数的参数值,也可以作为返回值。我们通常将作为变量的函数称为 “匿名函数” 或 Lambda 表达式。
在 Java 中如果我们要遍历这样一个列表:
1 | List<String> fruits = List.of("Apple", "Banana", "Orange", "Pear", "Grape"); |
可能会这样写:
1 | for(int i = 0; i< fruits.size(); i++) { |
如果了解迭代器 Iterator
也许会这样写:
1 | for(String fruit : fruits) { |
像是 JetBrains IDEA 这样的智能 IDE 有时会建议我们将上述写法转换为如下写法:
1 | fruits.forEach(fruit -> { |
这便是对 Lambda 表达式的一次简单使用。
接着我们来看一下 Java 中 Lambda 表达式的基本结构:
1 | (arg1, arg2, ...) -> { |
可以看到这和一个普通函数有一些相似之处,它通过一个参数列表来传递参数,这些参数甚至不需要指定参数类型,后面加上一个箭头 ->
,接着是对这个这些参数的一些操作,如果操作有多行那么需要用大括号 {}
括起来,否则不用。和普通函数类似;执行完函数体内的逻辑后可以用 return
从函数中返回。
fruits.forEach(fruit -> System.out.println(fruit);
这句话从语义上非常容易理解:fruit
调用了 List
接口的方法 forEach
,传入一个 lambda
表达式作为参数。可以想象 forEach
函数内部对 fruits
进行了遍历,将每一个元素作为参数传入 lambda
表达式,在其内对这个元素进行一些操作。此处我们将每次传入的元素命名为 fruit
,然后打印这一个元素。
Lambda API
像 .forEach
这样一个接受 Lambda 表达式的方法叫做 Lambda API。同其他支持函数式编程的语言一样,Java 提供了几个经典的 Lambda API : map
, filter
, reduce
。
使用 Stream
在使用这些 API 之前我们需要先了解 Java 的 Stream API ,因为 Lambda API 大都和来自 Stream API 而不是 List
等接口,在使用这些 API 时,需要先将 List
转换为 Stream
。
Stream
和 List
有亿点点不一样。List
中每个元素都是确定的存储在内存中的;而 Stream
可以是一个有确定元素的列表,也可以是一个形如 “全体” 自然数集合的抽象列表,操作时使用 limit
等方法将其截断转换为一个有确定元素的 Stream
。
Stream
的详细内容不是本文的重点,通常我们将其看成一种特殊的列表就好了,但是我们还是在此举一个使用 Stream
的例子:
1 | // 获取全体自然数 |
在 forEach
中传入 System.out::println
看起来很奇怪也有点恐怖,因为这是后文的方法引用相关内容,此处我们只需要知道这样写可以打印数据就行了。
map
filter
和 reduce
方法
上图对 map
、 filter
和 reduce
的解释可以说是 “不言而喻,一目了然” 了,此处我们直接看代码。
1 | List<Integer> listMap = list.stream().map(current -> current * 2).toList(); |
方法引用
lambda 表达式实际上也是一个函数,这意味着 lambda API 不仅能接受 lambda 表达式,也能接受函数,比如我们可以通过 .forEach
方法打印一个 list
的所有成员:
1 | List<Integer> list = List.of(3, 1, 4, 3, 2); |
它将遍历 list
将每个元素传入 lambda
表示,我们的 lambda
表达式只是简单调用 System.out.println
,将这个参数传入而已。这个方法的签名和 lambda
表达式完全一致,那么我们久可以把这个函数直接传入 lambda API 。以下代码和上述代码等效:
1 | List<Integer> list = List.of(3, 1, 4, 3, 2); |
这种将相同签名的方法传入 lambda API 的操作被称为方法引用。
自定义 Lambda API
Java 标准库提供了很多 lambda API 。我们当然也可以编写自己的 lambda API。
Java 的 lambda 表达式实际上是使用单方法接口实现的。调用 lambda API 并传入 lambda 表达式时,实际上是将 lambda 表达式作为一个单方法接口的实现,然后传入这个接口的一个实例。
比如我们想实现自己的 forEach
函数,这个函数接受需要迭代的对象,还接受对每个对象的处理逻辑,后者就是一个签名为 <T> void (T)
的 lambda 表达式。forEach
接口的签名可以是 <T> void forEach(List<T>, MyForEach<T>)
,MyForEach<T>
是一个单方法接口,又称函数式接口,声明这个接口时应使用 @FunctionalInterface
注解:
1 |
|
客户端使用时可以这样:
1 | public class Main { |
它等价于传统的写法:
1 | public class Main { |