Java函数式编程快速入门: Lambda表达式与Stream API

函数式编程(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)

传统思路要创建中间变量,要分步执行,而函数式编程的形式与数学表达式形式更为相似。人们普遍认为,这种函数式的描述更接近人类自然语言。

如果要实现这样一个函数式程序,主要需要两步:

  1. 实现单个函数,将零到多个输入转换成零到多个输出。比如add这种带有映射关系的函数,它将两个输入转化为一个输出。
  2. 将多个函数连接起来,实现所需业务逻辑。比如,将addmultiply连接到一起。

接下来我们通过Java语言来展示如何实践函数式编程思想。

Lambda表达式的构造

数理逻辑领域有一个名为λ演算的形式系统,主要研究如何使用函数来表达计算。一些编程语言将这个概念应用到自己的平台上,期望能实现函数式编程,取名为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表达式的一个拆解示意图,这很符合数学中对一个函数做映射的思维方式。

Java函数式编程快速入门: Lambda表达式与Stream API_第1张图片

接下来我们来了解一下输入参数和函数体的一些使用规范。

输入参数

  • Lambda表达式可以接收零到多个输入参数。
  • 程序员可以提供输入类型,也可以不提供类型,让编程语言根据上下文帮忙去推断。
  • 参数可以放在圆括号()中,多个参数通过英文逗号,隔开。如果只有一个参数,且类型可以被推断,可以不使用圆括号()。空圆括号表示没有输入参数。

函数体

  • 函数体可以有一到多行语句,是函数的核心处理逻辑。
  • 当函数体只有一行内容,且该内容正是需要输出的内容,可以不使用花括号{},直接输出。
  • 当函数体有多行内容,必须使用花括号{}
  • 输出的类型与所需要的类型相匹配。

至此,我们可以大致看出,Lambda表达式能够实现将零到多个输入转换为零到多个输出的映射,即实现了函数式编程的第一步,定义单个的函数。

Functional Interface

通过前面的几个例子,我们大概知道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包中,比如PredicateFunctionBinaryOperator等,开发者可以根据自己需求去实现这些接口。这里简单展示一下两个接口。

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其实就是这种函数式接口。

Java Stream API

流(Stream)是Java 8 的另外一大亮点,它与java.io包里的InputStreamOutputStream是完全不同的概念,也不是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类型,后经过filtermapcollect等处理逻辑,生成我们所需的输出。各个操作之间使用英文点号.来连接,这种方式被称作方法链(Method Chaining)或者链式调用。数据的链式调用可以被抽象成一个管道(Pipeline),如下图所示。

Java函数式编程快速入门: Lambda表达式与Stream API_第2张图片

我们深挖一下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天生就是为函数式编程所设计。由于函数式编程在并行处理方面的优势,正在被大量应用在大数据计算领域。

你可能感兴趣的:(Java/Scala手册)