4.5 lambda表达式

Java 8 引入的功能中,最让人期盼的是 lambda 表达式。lambda 表达式以字面量的形式把少量代码直接写在程序中,而且让 Java 编程更符合函数式风格。

其实,lambda 表达式的很多功能都能使用嵌套类型通过回调和处理程序等模式实现,但使用的句法总是非常冗长,尤其是,就算只需要在回调中编写一行代码,也要完整定义一个新类型。

第 2 章见过,lambda 表达式的句法是一个参数列表和方法主体,如下所示:

  1. (p, q) -> { /* 方法主体 */ }

这种句法能通过一种十分紧凑的方式表示简单的方法,而且能很大程度上避免使用匿名类。

4.5 lambda表达式 - 图1 组成方法的各个部分,lambda 表达式几乎都有,不过显然,lambda 表达式没有名称。其实,有些开发者喜欢把 lambda 表达式当成“匿名方法”。

例如,java.io.File 类的 list() 方法。这个方法列出一个目录中的文件,但在返回列表之前,要把每个文件的名称传给 FilenameFilter 对象,而这个对象必须由你提供。FilenameFilter 对象用于接受或拒绝各个文件。

使用匿名类可以按照如下的方式定义一个 FilenameFilter 类,只列出文件名以 .java 结尾的文件:

  1. File dir = new File("/src"); // 列出这个目录中的文件
  2. // 现在调用list()方法,参数的值是一个使用匿名类实现的FilenameFilter
  3. String[] filelist = dir.list(new FilenameFilter() {
  4. public boolean accept(File f, String s) {
  5. return s.endsWith(".java");
  6. }
  7. });

使用 lambda 表达式,上述代码可以简化成:

  1. File dir = new File("/src"); // 列出这个目录中的文件
  2. String[] filelist = dir.list((f,s) -> { return s.endsWith(".java"); });

对目录中的每个文件来说,都会执行 lambda 表达式中的代码。如果这个方法的返回值是 true,对应的文件就会出现在输出中,即存入数组 filelist 中。

这种模式叫过滤器,即使用一个代码块测试容器中的元素是否匹配某个条件,并且只返回能通过条件的元素。过滤器是函数式编程的标准技术之一,稍后会详细说明。

4.5.1 转换lambda表达式

javac 遇到 lambda 表达式时会把它解释为一个方法的主体,这个方法具有特定的签名。不过,是哪个方法呢?

为了解决这个问题,javac 会查看周围的代码。lambda 表达式必须满足以下条件才算是合法的 Java 代码:

  • lambda 表达式必须出现在期望使用接口类型实例的地方;

  • 期望使用的接口类型必须只有一个强制方法;

  • 这个强制方法的签名要完全匹配 lambda 表达式。

如果满足上述条件,编译器会创建一个类型,实现期望使用的接口,然后把 lambda 表达式的主体当作强制方法的实现。

说得稍微复杂一点儿,这么做是为了保持 Java 类型系统的名义(基于名称)纯粹性。也就是说,lambda 表达式会被转换成正确接口类型的实例。

有些开发者还喜欢使用“单一抽象方法”(Single Abstract Method,SAM)类型这个术语表示 lambda 表达式转换得到的接口类型。这表明,若想在 lambda 表达式机制中使用某个接口,这个接口必须只有一个非默认方法。

4.5 lambda表达式 - 图2 虽然 lambda 表达式和匿名类有很多相似之处,但 lambda 表达式并不只是匿名类的语法糖。其实,lambda 表达式使用方法句柄(第 11 章介绍)和一个特殊的新 JVM 字节码 invokedynamic 实现。

从上述讨论可以看出,Java 8 添加的 lambda 表达式经过精心设计,以适应 Java 现有的类型系统——这个系统十分注重名义类型。

4.5.2 方法引用

前面说过,可以把 lambda 表达式看成没有名称的方法。对下面的 lambda 表达式来说:

  1. // 实际上这行代码可以写得更简短,因为有类型推导
  2. (MyObject myObj) -> myObj.toString()

会自动转换成对 @FunctionalInterface 接口的实现,这个接口只有一个非默认方法,这个方法接受一个 MyObject 类型的参数,返回值类型为 String。不过,这里的样板代码太多,所以 Java 8 提供了一种句法,可以让这种 lambda 表达式更易于阅读和编写:

  1. MyObject::toString

这种简写形式叫方法引用(method reference),使用现有的方法作为 lambda 表达式。方法引用就像是使用现有的方法,但会忽略方法的名称,所以能作为 lambda 表达式使用,而且能使用往常的方式自动转换。

4.5.3 函数式编程

Java 实质上是面向对象语言。不过,引入 lambda 表达式后,可以更轻易地编写符合函数式风格的代码。

4.5 lambda表达式 - 图3 关于函数式语言由什么组成,没有明确的定义,但至少有一个共识:函数式语言最起码要能把函数当成值,存入变量。

Java(从 1.1 版起)一直都能通过内部类表示函数,但句法很复杂,代码结构不清晰。lambda 表达式大大简化了这种句法,因此,越来越多的开发者会在 Java 代码中寻求使用函数式编程风格,这是很自然的,而且现在做起来也更容易。

Java 开发者初尝函数式编程时有可能会使用如下三个非常有用的基本习语。

  • map()

map() 用于列表和类似列表的容器。运作原理是,传入一个函数,应用于集合中的各个元素,得到一个新集合。新集合中保存的是在各个元素上执行函数后得到的结果。这意味着,map() 可能会把一种类型的集合转换成另一种类型的集合。

  • filter()

说明如何把匿名类实现的 FilenameFilter 换成 lambda 表达式实现时,见过使用 filter() 的示例。filter() 基于某种条件生成一个集合的子集。注意,在函数式编程中,一般会生成新集合,而不直接修改现有的集合。

  • reduce()

reduce() 有几种不同的形式,执行的是聚合运算,除了叫化简之外,还可以叫合拢、累计或聚合。基本原理是,提供一个初始值和聚合函数(或化简函数),然后在各个元素上执行这个化简函数,在化简函数遍历整个集合的过程中会得到一系列中间值(类似于“累积计数”),最后得到一个最终结果。

Java 完全支持这些重要的函数式习语(除此之外还有几个)。第 8 章会稍微深入地说明这些习语的实现方式,届时会介绍 Java 的数据结构和集合,以及抽象。抽象流是实现这些习语的基础。

对 lambda 表达式的介绍到此结束,下面是一些注意事项。值得注意的是,最好把 Java 看成轻度支持函数式编程的语言。Java 不是专门的函数式语言,也不想变成函数式语言。Java 的某些特性决定了它不可能是函数式语言,具体而言有以下几点。

  • Java 没有结构类型,因此没有“真正的”函数类型。每个 lambda 表达式都会自动转换成适当的名义类型。

  • 类型擦除在函数式编程中会导致问题——高阶函数的类型安全性会丢失。

  • Java 天生可改变(第 6 章会介绍)——一般认为,可变性是函数式语言极不需要的特性。

抛开这些,能轻易使用基本的函数式编程风格,尤其是 map()filter()reduce() 等习语,是 Java 社区向前迈出的一大步。这些习语非常有用,因此绝大多数 Java 开发者都不需要也不会错过纯正函数式语言提供的高级功能。