3.2 在哪里以及如何使用Lambda

现在你可能在想,在哪里可以使用Lambda表达式。在上一个例子中,你把Lambda赋给了一个Comparator类型的变量。你也可以在上一章中实现的filter方法中使用Lambda:

  1. List<Apple> greenApples =
  2. filter(inventory, (Apple a) -> GREEN.equals(a.getColor()));

那到底在哪里可以使用Lambda呢?你可以在函数式接口上使用Lambda表达式。在上面的代码中,可以把Lambda表达式作为第二个参数传给filter方法,因为它这里需要Predicate,而这是一个函数式接口。如果这听起来太抽象,不要担心,现在我们就来详细解释这是什么意思,以及函数式接口是什么。

3.2.1 函数式接口

还记得你在第2章里,为了参数化filter方法的行为而创建的Predicate接口吗?它就是一个函数式接口!为什么呢?因为Predicate仅仅定义了一个抽象方法:

  1. public interface Predicate<T>{
  2. boolean test (T t);
  3. }

一言以蔽之,函数式接口就是只定义一个抽象方法的接口。你已经知道了Java API中的一些其他函数式接口,如第2章中谈到的ComparatorRunnable

  1. public interface Comparator<T> { ←---- java.util.Comparator
  2. int compare(T o1, T o2);
  3. }
  4. public interface Runnable { ←---- java.lang.Runnable
  5. void run();
  6. }
  7. public interface ActionListener extends EventListener { ←---- java.awt.event.ActionListener
  8. void actionPerformed(ActionEvent e);
  9. }
  10. public interface Callable<V> { ←---- java.util.concurrent.Callable
  11. V call() throws Exception;
  12. }
  13. public interface PrivilegedAction<T> { ←---- java.security.PrivilegedAction
  14. T run();
  15. }

注意 你将会在第13章中看到,接口现在还可以拥有默认方法(即在类没有对方法进行实现时,其主体为方法提供默认实现的方法)。哪怕有很多默认方法,只要接口只定义了一个抽象方法,它就仍然是一个函数式接口。

为了检查你的理解程度,测验3.2将帮助你测试自己是否掌握了函数式接口的概念。

测验3.2:函数式接口

下面哪些接口是函数式接口?

  1. public interface Adder {
  2. int add(int a, int b);
  3. }
  4. public interface SmartAdder extends Adder {
  5. int add(double a, double b);
  6. }
  7. public interface Nothing {
  8. }

答案:只有Adder是函数式接口。

SmartAdder不是函数式接口,因为它定义了两个叫作add的抽象方法(其中一个是从Adder那里继承来的)。

Nothing也不是函数式接口,因为它没有声明抽象方法。

用函数式接口可以干什么呢?Lambda表达式允许你直接以内联的形式为函数式接口的抽象方法提供实现,并把整个表达式作为函数式接口的实例(具体说来,是函数式接口一个具体实现的实例)。你用匿名内部类也可以完成同样的事情,只不过比较笨拙:需要提供一个实现,然后再直接内联将它实例化。下面的代码是有效的,因为Runnable是只定义了一个抽象方法run的函数式接口:

  1. Runnable r1 = () -> System.out.println("Hello World 1"); ←---- 使用Lambda
  2. Runnable r2 = new Runnable(){ ←---- 使用匿名类
  3. public void run(){
  4. System.out.println("Hello World 2");
  5. }
  6. };
  7. public static void process(Runnable r){
  8. r.run();
  9. }
  10. process(r1); ←---- 打印“Hello World 1
  11. process(r2); ←---- 打印“Hello World 2
  12. process(() -> System.out.println("Hello World 3")); ←---- 利用直接传递的Lambda打印“Hello World 3

3.2.2 函数描述符

函数式接口的抽象方法的签名基本上就是Lambda表达式的签名。我们将这种抽象方法叫作函数描述符。例如,Runnable接口可以看作一个什么也不接受什么也不返回(void)的函数的签名,因为它只有一个叫作run的抽象方法,这个方法什么也不接受,什么也不返回(void)。1

1Scala等语言的类型系统提供显式类型标注,可以描述函数的类型(称为“函数类型”)。Java重用了函数式接口提供的标准类型,并将其映射成一种形式的函数类型。

本章中使用了一个特殊表示法来描述Lambda和函数式接口的签名。() -> void代表了参数列表为空且返回void的函数。这正是Runnable接口所代表的。再举一个例子,(Apple, Apple) -> int代表接受两个Apple作为参数且返回int的函数。3.4节和本章后面的表3-2中提供了关于函数描述符的更多信息。

你可能已经在想,Lambda表达式是怎么做类型检查的。3.5节会详细介绍编译器是如何检查Lambda在给定上下文中是否有效的。现在,只要知道Lambda表达式可以被赋给一个变量,或传递给一个接受函数式接口作为参数的方法就好了,当然这个Lambda表达式的签名要和函数式接口的抽象方法一样。比如,在之前的例子里,你可以像下面这样直接把一个Lambda传给process方法:

  1. public void process(Runnable r){
  2. r.run();
  3. }
  4. process(() -> System.out.println("This is awesome!!"));

此段代码执行时将打印“This is awesome!!”。Lambda表达式()-> System.out.println ("This is awesome!!")不接受参数且返回void。这恰恰是Runnable接口中run方法的签名。

Lambda及空方法调用

虽然下面这种Lambda表达式调用看起来很奇怪,但是合法的:

  1. process(() -> System.out.println("This is awesome"));

System.out.println返回void,所以很明显这不是一个表达式!为什么不像下面这样用花括号环绕方法体呢?

  1. process(() -> { System.out.println("This is awesome"); });

结果表明,方法调用的返回值为空时,Java语言规范有一条特殊的规定。这种情况下,你不需要使用括号环绕返回值为空的单行方法调用。

你可能会想:“为什么在只需要函数式接口的时候才可以传递Lambda呢?”语言的设计者也考虑过其他办法,例如给Java添加函数类型(有点儿像我们介绍描述Lambda表达式签名时的特殊表示法,第20章和第21章会继续讨论这个问题)。但是他们选择了现在这种方式,因为这种方式很自然,并且能避免让语言变得更复杂。此外,大多数Java程序员都已经熟悉了带有一个抽象方法的接口(譬如进行事件处理时)。然而,最重要的原因在于Java 8之前函数式接口就已经得到了广泛应用。这意味着,采用这种方式,遗留代码迁移到Lambda表达式的迁移路径会比较顺畅。实际上,你已经使用了函数式接口,像ComparatorRunnable,甚至你自己的接口,如果只定义了一个抽象方法,都算是函数式接口。你可以使用Lambda表达式替换他们,而无须修改你的API。试试看测验3.3,测试一下你对哪里可以使用Lambda这个知识点的掌握情况。

测验3.3:在哪里可以使用Lambda

以下哪些是使用Lambda表达式的有效方式?

(1)

  1. execute(() -> {});
  2. public void execute(Runnable r){
  3. r.run();
  4. }

(2)

  1. public Callable<String> fetch() {
  2. return () -> "Tricky example ;-)";
  3. }

(3)

  1. Predicate<Apple> p = (Apple a) -> a.getWeight();

答案:只有(1)和(2)是有效的。

第(1)个例子有效,是因为Lambda() -> {}具有签名() -> void,这和Runnable中的抽象方法run的签名相匹配。请注意,此代码运行后什么都不会做,因为Lambda是空的!

第(2)个例子也是有效的。事实上,fetch方法的返回类型是CallableCallable基本上就定义了一个方法,签名是() -> String,其中TString代替了。因为Lambda() -> "Trickyexample;-)"的签名是() -> String,所以在这个上下文中可以使用Lambda。

第(3)个例子无效,因为Lambda表达式(Apple a) -> a.getWeight()的签名是(Apple) -> Integer,这和Predicate: (Apple) -> boolean中定义的test方法的签名不同。

 

@FunctionalInterface又是怎么回事?

如果你去看看新的Java API,会发现函数式接口带有@FunctionalInterface的标注(3.4节中会深入研究函数式接口,并会给出一个长长的列表)。这个标注用于表示该接口会设计成一个函数式接口,因此对文档来说非常有用。此外,如果你用@FunctionalInterface定义了一个接口,而它不是函数式接口的话,编译器将返回一个提示原因的错误。例如,错误消息可能是“Multiple non-overriding abstract methods found in interface Foo”,表明存在多个抽象方法。请注意,@FunctionalInterface不是必需的,但对于为此设计的接口而言,使用它是比较好的做法。它就像是@Override标注表示方法被重写了。