函数式编程(Functional Programming)是一种编程范式。它已经有近60年的历史,因其更适合做并行计算,近年来开始受到大数据开发者的广泛关注。Python、JavaScript等当红语言对函数式编程支持都不错,Scala更是以函数式编程的优势在大数据领域攻城略地,即使是老牌的Java为了适应函数式编程,也加大对函数式编程的支持。未来的程序员或多或少都要了解一些函数式编程思想。本文抛开一些数学推理等各类复杂的概念,从使用的角度带领读者入门函数式编程。
在介绍函数式编程前,我们可以先回顾传统的编程范式如何解决一个数学问题。假设我们想求解一个数学表达式:
(x + y) * z
一般的编程思路是:
addResult = x + y
result = addResult * 3
在这个例子中,我们要先求解中间结果,存储到中间变量,再进一步求得最终结果。这仅仅是一个简单的例子,在更多的编程实践中,程序员必须告诉计算机每一步去执行什么命令,需要声明哪些中间变量等。因为计算机无法理解复杂的概念,只能听从程序员的指挥。
中学时代,我们的数学课上曾花费大量时间讲解函数,函数 y = f ( x ) y = f(x) y=f(x)指对于自变量x的映射。函数式编程的思想正是基于数学中对函数的定义。其基本思想是,在使用计算机求解问题时,我们可以把整个计算过程定义为不同的函数。比如,将这个问题转化为:
result = multiply(add(x, y), z)
我们再做进一步的转换:
result = add(x, y).multiply(z)
传统思路要创建中间变量,要分步执行,而函数式编程的形式与数学表达式形式更为相似。人们普遍认为,这种函数式的描述更接近人类自然语言。
如果要实现这样一个函数式程序,主要需要两步:
add
这种带有映射关系的函数,它将两个输入转化为一个输出。add
、multiply
连接到一起。接下来我们通过Java语言来展示如何实践函数式编程思想。
数理逻辑领域有一个名为λ演算的形式系统,主要研究如何使用函数来表达计算。一些编程语言将这个概念应用到自己的平台上,期望能实现函数式编程,取名为Lambda表达式(λ的英文拼写为Lambda)。
我们先看一下Java的Lambda表达式的语法规则:
(parameters) -> {
body
}
Lambda表达式主要包括一个箭头->
符号,两边连接着输入参数和函数体。我们再看看几个Lambda表达式的例子:
// 1. 无参数,返回值为5
() -> 5
// 2. 接收一个参数(int类型),将其乘以2,返回一个int
x -> 2 * x
// 3. 接受2个参数(int类型),返回他们的差
(x, y) -> x – y
// 4. 接收2个int型整数,返回他们的和
(int x, int y) -> x + y
// 5. 接受一个String对象,在控制台打印,不返回任何值
(String s) -> { System.out.print(s); }
// 6. 参数为圆半径,返回圆面积,返回值为double类型
(double r) -> {
double pi = 3.1415;
return r * r * pi;
}
可以看到,这几个例子都有一个->
,表示这是一个函数式的映射,相对比较灵活的是左侧的输入参数和右侧的函数体。下图为Java Lambda表达式的一个拆解示意图,这很符合数学中对一个函数做映射的思维方式。
接下来我们来了解一下输入参数和函数体的一些使用规范。
()
中,多个参数通过英文逗号,
隔开。如果只有一个参数,且类型可以被推断,可以不使用圆括号()
。空圆括号表示没有输入参数。{}
,直接输出。{}
。至此,我们可以大致看出,Lambda表达式能够实现将零到多个输入转换为零到多个输出的映射,即实现了函数式编程的第一步,定义单个的函数。
通过前面的几个例子,我们大概知道Lambda表达式的内部结构了,那么Lambda表达式到底是什么类型呢?在Java中,Lambda表达式是有类型的,它是一个interface
。确切的说,Lambda表达式实现了一个函数式接口(Functional Interface),或者说,前面提到的一些Lambda表达式都是函数式接口的具体实现。
函数式接口是一种interface
,并且它只有一个虚函数。因为这种interface
只有一个虚函数,因此英文中被称为Single Abstract Method(SAM)类型接口,这也意味着这个接口对外只提供这一个函数的功能。如果我们想自己设计一个函数式接口,我们应该给这个接口添加@FunctionalInterface
注解。编译器会根据这个注解确保该接口确实是函数式接口,当我们尝试往该接口中添加超过一个虚函数方法时,编译器会报错。下面的例子中,我们自己设计一个加法的函数式接口AddInterface
,然后实现这个接口。
关于interface
、
泛型等知识,可以参考我前两篇文章:继承和泛型。
@FunctionalInterface
interface AddInterface<T> {
T add(T a, T b);
}
public class FunctionalInterfaceExample {
public static void main( String[] args ) {
AddInterface<Integer> addInt = (Integer a, Integer b) -> a + b;
AddInterface<Double> addDouble = (Double a, Double b) -> a + b;
int intResult;
double doubleResult;
intResult = addInt.add(1, 2);
doubleResult = addDouble.add(1.1d, 2.2d);
}
}
有了函数式接口的定义,我们知道在实现一个Lambda表达式时,Lambda表达式实际上是在实现这个函数式接口中的虚函数,Lambda表达式的输入类型和返回类型要与虚函数定义的类型相匹配。
假如没有Lambda表达式,我们仍然可以实现这个函数式接口,只不过代码比较臃肿。首先,我们需要声明一个类来实现这个接口,可以是下面这样的一个类:
public static class MyAdd implements AddInterface<Double> {
@Override
public Double add(Double a, Double b) {
return a + b;
}
}
在业务逻辑中这样调用:doubleResult = new MyAdd().add(1.1d, 2.2d);
。或者是使用匿名类,连MyAdd
这个名字省去,直接实现AddInterface
并调用:
doubleResult = new AddInterface<Double>(){
@Override
public Double add(Double a, Double b) {
return a + b;
}
}.add(1d, 2d);
声明类并实现接口和使用匿名类这两种方法是Lambda表达式出现之前Java开发者经常使用的两种方法。实际上我们想实现的逻辑仅仅是一个a + b
,其他行代码其实都是冗余的,都是为了给编译器看的,并不是为了给程序员看的。有了比较我们会发现,Lambda表达式简洁优雅的优势就凸显出来了。
为了方便大家使用,Java内置了一些的函数式接口,放在java.util.function
包中,比如Predicate
、Function
、BinaryOperator
等,开发者可以根据自己需求去实现这些接口。这里简单展示一下两个接口。
Predicate
对输入进行判断,符合给定逻辑则返回true
,否则返回false
。
@FunctionalInterface
public interface Predicate<T> {
// 判断输入的真假,返回boolean
boolean test(T t);
}
Function
接收一个类型T的输入,返回一个类型R的输出。
@FunctionalInterface
public interface Function<T, R> {
// 接收一个类型T的输入,返回一个类型R的输出
R apply(T t);
}
一些底层的框架性代码提供了一些函数式接口供开发者调用,很多框架提供给开发者的API其实就是类似上面的函数式接口,开发者通过实现接口来完成自己的业务逻辑。Spark和Flink对外提供的Java API其实就是这种函数式接口。
流(Stream)是Java 8 的另外一大亮点,它与java.io
包里的InputStream
和OutputStream
是完全不同的概念,也不是Flink、Kafka等大数据实时处理中的数据流。它专注于对集合(Collection)对象的操作,是借助Lambda表达式的一种应用。通过Java Stream,我们可以体验到Lambda表达式带来的编程效率的提升。
我们看一个简单的例子,这个例子首先过滤出非空字符串,然后求得每个字符串的长度,最终返回为一个List
。代码使用了Lambda表达式来完成对应的逻辑。
List<String> strings = Arrays.asList(
"abc", "", "bc", "12345",
"efg", "abcd","", "jkl");
List<Integer> lengths = strings
.stream()
.filter(string -> !string.isEmpty())
.map(s -> s.length())
.collect(Collectors.toList());
lengths.forEach((s) -> System.out.println(s));
这段代码中,数据先经过stream
方法被转换为一个Stream
类型,后经过filter
、map
、collect
等处理逻辑,生成我们所需的输出。各个操作之间使用英文点号.
来连接,这种方式被称作方法链(Method Chaining)或者链式调用。数据的链式调用可以被抽象成一个管道(Pipeline),如下图所示。
我们深挖一下Java Stream的源码,发现filter
的参数正是前文所说的Predicate
函数式接口,map
的参数是前文提到的Function
函数式接口。当处理具体的业务时,就是使用Lambda表达式来实现这些函数式接口。
Stream<T> filter(Predicate<? super T> predicate);
<R> Stream<R> map(Function<? super T, ? extends R> mapper);
上面两行是Java Stream的源码,其中?
是泛型通配符,主要是为了对泛型做一些安全性上的限制,有兴趣的读者可以自行去了解泛型的的通配符。
Java Stream是应用Lambda表达式的最佳案例,Stream管道和链式调用解决了本文最初提到的函数式编程第二个问题:将多个函数连接起来,实现所需业务逻辑。
函数式编程更符合数学上函数映射的思想。具体到编程语言层面,我们可以使用Lambda表达式来快速编写函数映射,函数之间通过链式调用连接到一起,完成所需业务逻辑。Java的Lambda表达式是后来才引入的,而Scala天生就是为函数式编程所设计。由于函数式编程在并行处理方面的优势,正在被大量应用在大数据计算领域。