前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Java函数式编程快速入门: Lambda表达式与Stream API

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

作者头像
PP鲁
发布2020-02-26 15:33:08
1.1K0
发布2020-02-26 15:33:08
举报
文章被收录于专栏:皮皮鲁的AI星球皮皮鲁的AI星球

函数式编程(Functional Programming)是一种编程范式。它已经有近60年的历史,因其更适合做并行计算,近年来开始受到大数据开发者的广泛关注。Python、JavaScript等当红语言对函数式编程支持都不错,Scala更是以函数式编程的优势在大数据领域攻城略地,即使是老牌的Java为了适应函数式编程,也加大对函数式编程的支持。未来的程序员或多或少都要了解一些函数式编程思想。本文抛开一些数学推理等各类复杂的概念,从使用的角度带领读者入门函数式编程。

函数式编程思想

在介绍函数式编程前,我们可以先回顾传统的编程范式如何解决一个数学问题。假设我们想求解一个数学表达式:

代码语言:javascript
复制
(x + y) * z

一般的编程思路是:

代码语言:javascript
复制

addResult = x + y
result = addResult * 3

在这个例子中,我们要先求解中间结果,存储到中间变量,再进一步求得最终结果。这仅仅是一个简单的例子,在更多的编程实践中,程序员必须告诉计算机每一步去执行什么命令,需要声明哪些中间变量等。因为计算机无法理解复杂的概念,只能听从程序员的指挥。

中学时代,我们的数学课上曾花费大量时间讲解函数,函数y = f(x)指对于自变量x的映射。函数式编程的思想正是基于数学中对函数的定义。其基本思想是,在使用计算机求解问题时,我们可以把整个计算过程定义为不同的函数。比如,将这个问题转化为:

代码语言:javascript
复制

result = multiply(add(x, y), z)

我们再做进一步的转换:

代码语言:javascript
复制
result = add(x, y).multiply(z)

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

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

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

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

Lambda表达式的构造

数理逻辑领域有一个名为λ演算的形式系统,主要研究如何使用函数来表达计算。一些编程语言将这个概念应用到自己的平台上,期望能实现函数式编程,取名为Lambda表达式(λ的英文拼写为Lambda)。

我们先看一下Java的Lambda表达式的语法规则:

代码语言:javascript
复制
(parameters) -> {
  body 
}

Lambda表达式主要包括一个箭头->符号,两边连接着输入参数和函数体。我们再看看几个Lambda表达式的例子:

代码语言:javascript
复制
// 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表达式能够实现将零到多个输入转换为零到多个输出的映射,即实现了函数式编程的第一步,定义单个的函数。

Functional Interface

通过前面的几个例子,我们大概知道Lambda表达式的内部结构了,那么Lambda表达式到底是什么类型呢?在Java中,Lambda表达式是有类型的,它是一个interface。确切的说,Lambda表达式实现了一个函数式接口(Functional Interface),或者说,前面提到的一些Lambda表达式都是函数式接口的具体实现。

函数式接口是一种interface,并且它只有一个虚函数。因为这种interface只有一个虚函数,因此英文中被称为Single Abstract Method(SAM)类型接口,这也意味着这个接口对外只提供这一个函数的功能。如果我们想自己设计一个函数式接口,我们应该给这个接口添加@FunctionalInterface注解。编译器会根据这个注解确保该接口确实是函数式接口,当我们尝试往该接口中添加超过一个虚函数方法时,编译器会报错。下面的例子中,我们自己设计一个加法的函数式接口AddInterface,然后实现这个接口。

关于interface<T>泛型等知识,可以参考我前两篇文章:继承泛型

代码语言:javascript
复制
@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表达式,我们仍然可以实现这个函数式接口,只不过代码比较臃肿。首先,我们需要声明一个类来实现这个接口,可以是下面这样的一个类:

代码语言:javascript
复制
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并调用:

代码语言:javascript
复制
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

代码语言:javascript
复制

@FunctionalInterface
public interface Predicate<T> {
    // 判断输入的真假,返回boolean
    boolean test(T t);
}

Function接收一个类型T的输入,返回一个类型R的输出。

代码语言:javascript
复制

@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<Integer>。代码使用了Lambda表达式来完成对应的逻辑。

代码语言:javascript
复制
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 Stream的源码,发现filter的参数正是前文所说的Predicate函数式接口,map的参数是前文提到的Function函数式接口。当处理具体的业务时,就是使用Lambda表达式来实现这些函数式接口。

代码语言:javascript
复制
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天生就是为函数式编程所设计。由于函数式编程在并行处理方面的优势,正在被大量应用在大数据计算领域。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-02-14,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 皮皮鲁的AI星球 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 函数式编程思想
  • Lambda表达式的构造
    • 输入参数
      • 函数体
      • Functional Interface
      • Java Stream API
      • 小结
      相关产品与服务
      大数据
      全栈大数据产品,面向海量数据场景,帮助您 “智理无数,心中有数”!
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档