lambda即匿名函数,使用它可以简洁的表示一个行为。由于这个“行为”是可以传递的,Java8的世界变得妙极了~
引言
简单地看个例子:
1 |
|
1 | appleList.sort(new Comparator<Apple>() { |
sort方法需要传入一个Comparator接口以便进行排序。
在Java8以前,显然需要传入一个Comparator的实现类,就像上面的代码。
我们已经习惯了使用匿名类来实现接口,但是这种方式有几个显而易见的缺点:
-
代码量太多,我们真正要关心的只有
o1.getWeight() - o2.getWeight()
一句,其他的代码都是一些不得不写,但是很繁琐的样板化代码。而且对于阅读代码来说也是写无用信息。 -
如果需要不同的比较器,需要书写多个匿名类。而且,为了复用和清晰,我们就不能使用匿名类,只得将他们提取出来,成为一个个的比较器实现类,这又增加了许多代码的书写,如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13class AppleComparatorByWeight implements Comparator<Apple>{
public int compare(Apple a1, Apple a2) {
return xxx
}
}
class AppleComparatorByColor implements Comparator<Apple>{
public int compare(Apple a1, Apple a2) {
return xxx
}
}
appleList.sort(new AppleComparatorByWeight());
appleList.sort(new AppleComparatorByColor());
看看Java8是如何解决的:
1 | appleList.sort((a1, a2) -> a1.getWeight() - a2.getWeight()); |
使用Lambd后,只需要关注真正的业务代码,它自动实现了Comparator接口,并复写了compare方法,我们再也不用写那些讨厌的样板化代码了。代码的语言也变得清晰易懂。
除了Lambda,使用方法引用
,甚至可以进一步将代码简化如下:
1 | appleList.sort(comparingInt(Apple::getWeight)); |
要复用比较器的情景下,也不需要编写实现类,只需要用lambda。
1 | Comparator<Apple> c1 = (a1, a2) -> a1.getWeight() - a2.getWeight(); |
函数式接口 @FunctionalInterface
@FunctionalInterface
是Java8新引入的一个注解,它可以标记在接口/抽象类上,表明此接口/抽象类是一个函数式接口
。如果该接口/抽象类不满足函数式接口的要求,那么编译器将报错。
函数式接口: 该接口/抽象类只包含一个抽象方法。(default方法不算抽象方法)
比如刚才的sort方法接收的Comparator接口,就是一个函数式接口,它只有一个compare()抽象方法。
注:如果一个接口复写了Object的方法,并不影响它成为函数式接口。比如Comparator接口还复写了equals方法。
在比如Runnable接口,在JDK8中也被标记上了@FunctionalInterface
,因为它只有一个抽象方法:
1 |
|
那么,函数式接口有什么用呢?
传递行为,而不是传递值
Java8中,终于将函数/lambda作为一等公民,这意味着,我们可以将“行为”保存到一个变量里;也可以将"行为"作为参数,在方法之间传递。
我们先自己写一段代码来感受下,如何接收一个行为。
1 |
|
显然,lambda自动地实现了BufferedReaderProcessor接口并override了process方法。
在看下其他的例子,
1 | //执行一个行为是“打印haha"的线程 |
我们说过Runnable接口是一个FunctionalInterface,那么第一个例子中,lambda显然实现了Runnable的run方法。
那么,传递给forEach方法的lambda又实现了什么接口呢?
1 | // public interface Iterable<T> |
可以看到,forEach方法接收一个consumer,那么显然我们传递的apple -> System.out.println(apple)
实现了该接口。
1 |
|
Consumer只有一个抽象方法accept。它接收一个t,返回void。
结合forEach()方法的定义来思考,当我们调用appleList.forEach(apple -> System.out.println(apple));
方法时,t其实是list中的每个apple元素,action是打印apple。
于是,对于每个apple,Consumer(即“打印行为”) accpet了这个apple。
这里有可能有点绕,其实就像下面:
1 | appleList.forEach(new Consumer<Apple>() { |
lambda相当于实现了Cousumer接口的accpet方法,然后forEach方法中,对每个元素,都调用accept方法。
Consumer其实是JDK为我们抽象出了一个通用的函数接口,他将所有接收一个对象,返回void的行为,叫做Consumer
。
又比如Predicate
接口
1 |
|
接收一个对象,返回boolean的行为,叫做Predicate。
比如我们可以这样:
1 | Object[] objects = appleList.stream() |
filter方法接收一个Predicate
。apple为红色时,返回true。
那么这条代码将返回appleList中所有红色的苹果。
JDK中还有许多抽象的行为接口:
函数式接口 | 函数描述符 | 说明 |
---|---|---|
Predicate |
T->boolean | 接收T,返回boolean |
Consumer |
T->void | 接收T,返回void |
Function<T,R> | T->R | 接收T,返回R |
Supplier |
()->T | 什么也不接收,返回一个T |
UnaryOperator |
T->T | 接收一个T,返回一个T |
BinaryOperator |
(T,T)->T | 接收两个T类型对象,返回一个T类型对象 |
BiPredicate<L,R> | (L,R)->boolean | 接收T、R,返回boolean |
BiConsumer<T,U> | (T,U)->void | 接收T、U,返回void |
BiFunction<T,U,R> | (T,U)->R | 接收T、U,返回一个R |
还记得上面我们实现的BufferedReaderProcessor
吗?
1 |
|
它不正是一个Function<BufferedReader,String>吗。TODO 试着修改下之前的代码?
lambda书写语法
Lambda基本语法是
(parameters) -> expression
或(请注意语句的花括号)
(parameters) -> { statements; }
1 | //第一个Lambda具有一个String类型的参数并返回一个int。 没有写return语句,但是已经隐含了return |
方法引用
我们已经知道了,可以通过lambda来implement一个函数式接口,从而省去很多样板化代码。
另外,通过方法引用的方式,也可以implement一个函数式接口,而且这种方式能进一步简化代码、提高代码易读性。
1 | // 例一:静态方法引用 |
以上展示了总共的三种方法引用。
- 静态方法的引用
- 任意类型实例方法的引用
- 现有对象的方法的引用
需要区别的是后两种,正如实例代码中,第二种方式时,appleList中的每个元素是方法的调用者;而第三种方式时,由于引用的是其他对象的方法,所以appleList中的每个元素成为了该方法的入参。
ps:实例代码中的map方法传入一个Function<T,R>,作用是将一个T类型的流映射成一个R类型的流。关于流的内容请参考下一讲《Java8-Stream》
进阶:复合lambda
类比于数学中的函数组合
1 | f(x) = x+1 |
可以把多个简单的lambda复合成复杂的表达式。
函数复合
1 | Function<Integer, Integer> f = x -> x + 1; |
谓词复合
1 | Predicate<Apple> redApple = apple -> "red".equals(apple.getColor()); |
比较器复合
逆序
1 | appleList.sort( comparing(Apple::getWeight).reversed() ); |
比较器链
1 | appleList.sort( |
lambda的注意事项
lambda与异常
请注意,任何函数式接口都不允许抛出受检异常(checked exception)。如果你需要Lambda 表达式来抛出异常,有两种办法:定义一个自己的函数式接口,并声明受检异常,或者把Lambda 包在一个try/catch块中。
- 对于自己的函数式接口
比如,我们介绍了一个新的函数式接口BufferedReaderProcessor,它显式声明了一个IOException:
1 |
|
- 把Lambda在一个try/catch块中
如果你使用一个接受函数式接口的API,比如Function<T, R>,没有办法自己声明抛出Exception。这种情况下,你可以显式捕捉受检异常:
1 | Function<BufferedReader, String> f = (BufferedReader b) -> { |
同样的 Lambda,不同的函数式接口
同一个Lambda表达式就可以对应不同的函数式接口,只要它们的方法签名能够兼容 。
比如我们自己写的BufferedReaderProcessor其实就是一个Function<BufferedReader, String>
1 |
|
于是相同的lambda可以同时对应BufferedReaderProcessor和Function<BufferedReader, String>
1 | BufferedReaderProcessor p1 = br -> br.readLine(); |
再比如:
1 | Comparator<Apple> c1 = |
(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
的方法签名为(Apple, Apple) -> Integer
,所以它同时符合三种函数式接口的签名
- Comparator: ( T, T ) ->int
- ToIntBiFunction: (T, U)-> int
- BiFunction: ( T, U, R ) -> R
另外,如果一个Lambda的主体是一个语句表达式, 它就和一个返回void的函数描述符兼容(当然需要参数列表也兼容)。例如,以下两行都是合法的,尽管List的add方法返回了一个boolean,而不是Consumer上下文(T -> void)所要求的void:
1 | // Predicate返回了一个boolean |
类型推断
编译器可以了解Lambda表达式的参数类型,这样就可在Lambda语法中省去标注参数类型
1 | Comparator<Apple> c = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()); |
局部变量
Lambda表达式引用的局部变量必须是 final,或事实上是final 的
下面的代码无法编译,因为portNumber被赋值两次
1 | int portNumber = 1337; |
你可能会问自己,为什么局部变量有这些限制。
第一,实例变量和局部变量背后的实现有一个关键不同。实例变量都存储在堆中,而局部变量则保存在栈上。如果Lambda可以直接访问局部变量,而且Lambda是在一个线程中使用的,则使用Lambda的线程,可能会在分配该变量的线程将这个变量收回之后,去访问该变量。因此, Java在访问自由局部变量时,实际上是在访问它的副本,而不是访问原始变量。如果局部变量仅仅赋值一次那就没有什么区别了——因此就有了 这个限制。
第二,这一限制不鼓励你使用改变外部变量的典型命令式编程模式(我们会在以后解释,这种模式会阻碍很容易做到的并行处理)
测验
函数式接口
布尔表达式 | (List |
Predicate<List |
---|---|---|
创建对象 | () -> new Apple(10) | Supplier |
消费一个对象 | (Apple a) -> System.out.println(a.getWeight()) | Consumer |
从一个对象中选择/提取 | (String s) -> s.length() | Function<String, Integer>或ToIntFunction |
合并两个值 | (int a, int b) -> a * b | IntBinaryOperator |
比较两个对象 | (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()) | Comparator |
下面哪些接口是函数式接口?
1 | public interface Adder{ |
答案:只有Adder是函数式接口。 SmartAdder不是函数式接口,因为它定义了两个叫作add的抽象方法(其中一个是从 Adder那里继承来的)。 Nothing也不是函数式接口,因为它没有声明抽象方法。
Lambda语法
根据上述语法规则,以下哪个不是有效的Lambda表达式?
1 | (1) () -> {} |
答案:只有4和5是无效的Lambda。
(1) 这个Lambda没有参数,并返回void。它类似于主体为空的方法:public void run() {}
。
(2) 这个Lambda没有参数,并返回String作为表达式。
(3) 这个Lambda没有参数,并返回String(利用显式返回语句)。
(4) return是一个控制流语句。要使此Lambda有效,需要使花括号,如下所示:(Integer i) -> {return "Alan" + i;}
。
(5)“Iron Man”是一个表达式,不是一个语句。要使此Lambda有效,你可以去除花括号和分号,如下所示: (String s) -> "Iron Man"
。或者如果你喜欢,可以使用显式返回语句,如下所示:(String s)->{return "IronMan";}
。
Comment