函数式编程(Functional Programming)是编程范式的一种,常见的编程范式还有面向过程编程、面向对象编程(OOP)、面向数据编程(DOP)等。

程序设计语言发展的早期,人们将一段封装起来可供调用的代码块称为 “过程” 或 “函数” ;当函数绑定到对象上用于对象的操作时又被称为 “方法” ;有人始终认为编程语言中应该有真正数学意义上的函数,它应该是一种,对于确定的输入,有确定的输出、没有副作用的纯函数。

Lambda 表达式

函数式编程的特点之一是,它将函数视作变量,可以作为函数的参数值,也可以作为返回值。我们通常将作为变量的函数称为 “匿名函数” 或 Lambda 表达式。

在 Java 中如果我们要遍历这样一个列表:

1
List<String> fruits = List.of("Apple", "Banana", "Orange", "Pear", "Grape");

可能会这样写:

1
2
3
for(int i = 0; i< fruits.size(); i++) {
System.out.println(fruits.get(i));
}

如果了解迭代器 Iterator 也许会这样写:

1
2
3
for(String fruit : fruits) {
System.out.println(fruit);
}

像是 JetBrains IDEA 这样的智能 IDE 有时会建议我们将上述写法转换为如下写法:

1
2
3
fruits.forEach(fruit -> {
System.out.println(fruit);
});

这便是对 Lambda 表达式的一次简单使用。

接着我们来看一下 Java 中 Lambda 表达式的基本结构:

1
2
3
(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

StreamList 有亿点点不一样。List 中每个元素都是确定的存储在内存中的;而 Stream 可以是一个有确定元素的列表,也可以是一个形如 “全体” 自然数集合的抽象列表,操作时使用 limit 等方法将其截断转换为一个有确定元素的 Stream

Stream 的详细内容不是本文的重点,通常我们将其看成一种特殊的列表就好了,但是我们还是在此举一个使用 Stream 的例子:

1
2
3
4
5
6
// 获取全体自然数
Stream<BigInteger> naturalNumbers = createNaturalStream();
naturalNumbers
.map(BigInteger::multiply) //全体自然数的乘积
.limit(100) // 截取前 100 个元素
.forEach(System.out::println); // 逐一打印每个元素

forEach 中传入 System.out::println 看起来很奇怪也有点恐怖,因为这是后文的方法引用相关内容,此处我们只需要知道这样写可以打印数据就行了。

map filterreduce 方法

图 1

上图对 mapfilterreduce 的解释可以说是 “不言而喻,一目了然” 了,此处我们直接看代码。

1
2
3
4
5
6
7
8
9
10
11
12
List<Integer> listMap = list.stream().map(current -> current * 2).toList();
for (Integer n : listMap)
System.out.printf("%d ", n);
System.out.println(); // 6 2 8 6 4

List<Integer> listFilter = list.stream().filter(current -> current > 2).toList();
for (Integer n : listFilter)
System.out.printf("%d ", n);
System.out.println(); // 3 4 3

Integer listReduceResult = list.stream().reduce(0, (acc, current) -> acc + current);
System.out.println(listReduceResult); // 13

方法引用

lambda 表达式实际上也是一个函数,这意味着 lambda API 不仅能接受 lambda 表达式,也能接受函数,比如我们可以通过 .forEach 方法打印一个 list 的所有成员:

1
2
3
4
List<Integer> list = List.of(3, 1, 4, 3, 2);
list.forEach(current -> {
System.out.println(current);
});

它将遍历 list 将每个元素传入 lambda 表示,我们的 lambda 表达式只是简单调用 System.out.println ,将这个参数传入而已。这个方法的签名和 lambda 表达式完全一致,那么我们久可以把这个函数直接传入 lambda API 。以下代码和上述代码等效:

1
2
List<Integer> list = List.of(3, 1, 4, 3, 2);
list.forEach(System.out::println);

这种将相同签名的方法传入 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
2
3
4
@FunctionalInterface
interface MyForEach<T> {
void run(T current);
}

客户端使用时可以这样:

1
2
3
4
5
6
7
8
9
10
11
12
public class Main {
public static void main(String[] args) {
List<Integer> list = List.of(3, 1, 4, 3, 2);
forEach(list, current -> System.out.printf("%d ", current));
}

private static <T> void forEach(List<T> list, MyForEach<T> myForEach) {
for(T t : list) {
myForEach.run(t);
}
}
}

它等价于传统的写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Main {
public static void main(String[] args) {
List<Integer> list = List.of(3, 1, 4, 3, 2);
forEach(list, new MyForEach<Integer>() {
@Override
public void run(Integer current) {
System.out.printf("%d ", current);
}
});
}

private static <T> void forEach(List<T> list, MyForEach<T> myForEach) {
for(T t : list) {
myForEach.run(t);
}
}
}