1.1 lambda表达式

问题

用户希望在代码中使用 lambda 表达式。

方案

使用某种 lambda 表达式语法,并将结果赋给函数式接口类型的引用。

讨论

函数式接口是一种包含单一抽象方法(single abstract method)的接口。类通过为接口中的所有方法提供实现来实现任何接口,这可以通过顶级类(top-level class)、内部类甚至匿名内部类完成。

Runnable 接口为例,它从 Java 1.0 开始就已存在。该接口包含的单一抽象方法是 run,它不传入任何参数并返回 voidThread 类构造函数传入 Runnable 作为参数,例 1-1 显示了 Runnable 接口的匿名内部类实现。

例 1-1 Runnable 接口的匿名内部类实现

  1. public class RunnableDemo {
  2. public static void main(String[] args) {
  3. new Thread(new Runnable() {
  4. @Override
  5. public void run() {
  6. System.out.println(
  7. "inside runnable using an anonymous inner class");
  8. }
  9. }).start();
  10. }
  11. }

➊ 匿名内部类

匿名内部类语法以关键字 new 开头,后面跟着 Runnable 接口名以及英文小括号,表示定义一个实现该接口但没有显式名(explicit name)的类。大括号({})中的代码重写 run 方法,将字符串打印到控制台。

例 1-2 中的代码采用 lambda 表达式,对例 1-1 进行了改写。

例 1-2 在 Thread 构造函数中使用 lambda 表达式

  1. new Thread(() -> System.out.println(
  2. "inside Thread constructor using lambda")).start();

上述代码使用箭头将参数与函数体隔开(由于没有参数,这里只使用一对空括号)。可以看到,函数体只包含一行代码,所以不需要大括号。这种语法被称为 lambda 表达式。注意,任何表达式求值都会自动返回。在本例中,由于 println 方法返回的是 void,所以该表达式同样会返回 void,与 run 方法的返回类型相匹配。

lambda 表达式必须匹配接口中单一抽象方法签名的参数类型和返回类型,这被称为与方法签名兼容。因此,lambda 表达式属于接口方法的实现,可以将其赋给该接口类型的引用。

例 1-3 显示了赋给某个变量的 lambda 表达式。

例 1-3 将 lambda 表达式赋给变量

  1. Runnable r = () -> System.out.println(
  2. "lambda expression implementing the run method");
  3. new Thread(r).start();

1.1 lambda表达式 - 图1 Java 库中不存在名为 Lambda 的类,lambda 表达式只能被赋给函数式接口引用。

“将 lambda 表达式赋给函数式接口”与“lambda 表达式属于函数式接口中单一抽象方法的实现”表示相同的含义。我们可以将 lambda 表达式视为实现接口的匿名内部类的主体。这就是 lambda 表达式必须与抽象方法兼容的原因,其参数类型和返回类型必须匹配该方法的签名。注意,所实现方法的名称并不重要,它不会作为 lambda 表达式语法的一部分出现在代码中。

因为 run 方法不传入参数,并且返回 void,所以本例特别简单。函数式接口 java.io.FilenameFilter 从 Java 1.0 开始就是 Java 标准库的一部分,该接口的实例被用作 File.list 方法的参数,只有满足该方法的文件才会被返回。

根据 Javadoc 的描述,FilenameFilter 接口包含单一抽象方法 accept,它的签名如下:

  1. boolean accept(File dir, String name)

其中,File 参数用于指定文件所在的目录,String 用于指定文件名。

例 1-4 采用匿名内部类来实现 FilenameFilter 接口,只返回 Java 源文件。

例 1-4 FilenameFilter 的匿名内部类实现

  1. File directory = new File("./src/main/java");
  2.  
  3. String[] names = directory.list(new FilenameFilter() {
  4. @Override
  5. public boolean accept(File dir, String name) {
  6. return name.endsWith(".java");
  7. }
  8. });
  9. System.out.println(Arrays.asList(names));

➊ 匿名内部类

在例 1-4 中,如果文件名以 .java 结尾,accept 方法将返回 true,否则返回 false

而例 1-5 采用 lambda 表达式实现 FilenameFilter 接口。

例 1-5 FilenameFilter 接口的 lambda 表达式实现

  1. File directory = new File("./src/main/java");
  2.  
  3. String[] names = directory.list((dir, name) -> name.endsWith(".java"));
  4. System.out.println(Arrays.asList(names));
  5. }

➊ lambda 表达式

可以看到,代码要简单得多。参数包含在小括号中,但并未声明类型。在编译时,编译器发现 list 方法传入一个 FilenameFilter 类型的参数,从而获知其单一抽象方法 accept 的签名,进而了解 accept 的参数为 FileString,因此兼容的 lambda 表达式参数必须匹配这些类型。由于 accept 方法的返回类型是布尔值,所以箭头右侧的表达式也必须返回布尔值。

如例 1-6 所示,我们也可以在代码中指定数据类型。

例 1-6 具有显式数据类型的 lambda 表达式

  1. File directory = new File("./src/main/java");
  2.  
  3. String[] names = directory.list((File dir, String name) ->
  4. name.endsWith(".java"));

➊ 显式数据类型

此外,如果 lambda 表达式的实现多于一行,则需要使用大括号和显式返回语句,如例 1-7 所示。

例 1-7 lambda 代码块

  1. File directory = new File("./src/main/java");
  2.  
  3. String[] names = directory.list((File dir, String name) -> {
  4. return name.endsWith(".java");
  5. });
  6. System.out.println(Arrays.asList(names));

➊ 代码块语法

这就是 lambda 代码块(block lambda)。在本例中,虽然代码主体只有一行,但可以使用大括号将多个语句括起来。注意,不能省略 return 关键字。

lambda 表达式在任何情况下都不能脱离上下文存在,上下文指定了将表达式赋给哪个函数式接口。lambda 表达式既可以是方法的参数,也可以是方法的返回类型,还可以被赋给引用。无论哪种情况,赋值类型必须为函数式接口。