3.2 在哪里以及如何使用Lambda
现在你可能在想,在哪里可以使用Lambda表达式。在上一个例子中,你把Lambda赋给了一个Comparator类型的变量。你也可以在上一章中实现的filter方法中使用Lambda:
List<Apple> greenApples =filter(inventory, (Apple a) -> GREEN.equals(a.getColor()));
那到底在哪里可以使用Lambda呢?你可以在函数式接口上使用Lambda表达式。在上面的代码中,可以把Lambda表达式作为第二个参数传给filter方法,因为它这里需要Predicate,而这是一个函数式接口。如果这听起来太抽象,不要担心,现在我们就来详细解释这是什么意思,以及函数式接口是什么。
3.2.1 函数式接口
还记得你在第2章里,为了参数化filter方法的行为而创建的Predicate接口吗?它就是一个函数式接口!为什么呢?因为Predicate仅仅定义了一个抽象方法:
public interface Predicate<T>{boolean test (T t);}
一言以蔽之,函数式接口就是只定义一个抽象方法的接口。你已经知道了Java API中的一些其他函数式接口,如第2章中谈到的Comparator和Runnable。
public interface Comparator<T> { ←---- java.util.Comparatorint compare(T o1, T o2);}public interface Runnable { ←---- java.lang.Runnablevoid run();}public interface ActionListener extends EventListener { ←---- java.awt.event.ActionListenervoid actionPerformed(ActionEvent e);}public interface Callable<V> { ←---- java.util.concurrent.CallableV call() throws Exception;}public interface PrivilegedAction<T> { ←---- java.security.PrivilegedActionT run();}
注意 你将会在第13章中看到,接口现在还可以拥有默认方法(即在类没有对方法进行实现时,其主体为方法提供默认实现的方法)。哪怕有很多默认方法,只要接口只定义了一个抽象方法,它就仍然是一个函数式接口。
为了检查你的理解程度,测验3.2将帮助你测试自己是否掌握了函数式接口的概念。
测验3.2:函数式接口
下面哪些接口是函数式接口?
public interface Adder {int add(int a, int b);}public interface SmartAdder extends Adder {int add(double a, double b);}public interface Nothing {}答案:只有
Adder是函数式接口。
SmartAdder不是函数式接口,因为它定义了两个叫作add的抽象方法(其中一个是从Adder那里继承来的)。
Nothing也不是函数式接口,因为它没有声明抽象方法。
用函数式接口可以干什么呢?Lambda表达式允许你直接以内联的形式为函数式接口的抽象方法提供实现,并把整个表达式作为函数式接口的实例(具体说来,是函数式接口一个具体实现的实例)。你用匿名内部类也可以完成同样的事情,只不过比较笨拙:需要提供一个实现,然后再直接内联将它实例化。下面的代码是有效的,因为Runnable是只定义了一个抽象方法run的函数式接口:
Runnable r1 = () -> System.out.println("Hello World 1"); ←---- 使用LambdaRunnable r2 = new Runnable(){ ←---- 使用匿名类public void run(){System.out.println("Hello World 2");}};public static void process(Runnable r){r.run();}process(r1); ←---- 打印“Hello World 1”process(r2); ←---- 打印“Hello World 2”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方法:
public void process(Runnable r){r.run();}process(() -> System.out.println("This is awesome!!"));
此段代码执行时将打印“This is awesome!!”。Lambda表达式()-> System.out.println ("This is awesome!!")不接受参数且返回void。这恰恰是Runnable接口中run方法的签名。
Lambda及空方法调用
虽然下面这种Lambda表达式调用看起来很奇怪,但是合法的:
process(() -> System.out.println("This is awesome"));
System.out.println返回void,所以很明显这不是一个表达式!为什么不像下面这样用花括号环绕方法体呢?
process(() -> { System.out.println("This is awesome"); });结果表明,方法调用的返回值为空时,Java语言规范有一条特殊的规定。这种情况下,你不需要使用括号环绕返回值为空的单行方法调用。
你可能会想:“为什么在只需要函数式接口的时候才可以传递Lambda呢?”语言的设计者也考虑过其他办法,例如给Java添加函数类型(有点儿像我们介绍描述Lambda表达式签名时的特殊表示法,第20章和第21章会继续讨论这个问题)。但是他们选择了现在这种方式,因为这种方式很自然,并且能避免让语言变得更复杂。此外,大多数Java程序员都已经熟悉了带有一个抽象方法的接口(譬如进行事件处理时)。然而,最重要的原因在于Java 8之前函数式接口就已经得到了广泛应用。这意味着,采用这种方式,遗留代码迁移到Lambda表达式的迁移路径会比较顺畅。实际上,你已经使用了函数式接口,像Comparator、Runnable,甚至你自己的接口,如果只定义了一个抽象方法,都算是函数式接口。你可以使用Lambda表达式替换他们,而无须修改你的API。试试看测验3.3,测试一下你对哪里可以使用Lambda这个知识点的掌握情况。
测验3.3:在哪里可以使用Lambda
以下哪些是使用Lambda表达式的有效方式?
(1)
execute(() -> {});public void execute(Runnable r){r.run();}(2)
public Callable<String> fetch() {return () -> "Tricky example ;-)";}(3)
Predicate<Apple> p = (Apple a) -> a.getWeight();答案:只有(1)和(2)是有效的。
第(1)个例子有效,是因为Lambda
() -> {}具有签名() -> void,这和Runnable中的抽象方法run的签名相匹配。请注意,此代码运行后什么都不会做,因为Lambda是空的!第(2)个例子也是有效的。事实上,
fetch方法的返回类型是Callable。Callable基本上就定义了一个方法,签名是() -> String,其中T被String代替了。因为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标注表示方法被重写了。
