Java8 实战学习 — Lambda 表达式
上一章,我们学习了参数化代码的实现方法,这个逻辑的推导对我自己来说还是蛮有意义的,因为这将对我以后的代码编辑产生影响。
这一节我们继续学习,我们将学习 Lambda 表达式的具体使用。
Lambda 概述:
可以把Lambda表达式理解为简洁地表示可传递的匿名函数的一种方式:它没有名称,但它 有参数列表、函数主体、返回类型,可能还有一个可以抛出的异常列表。这个定义够大的,让我 们慢慢道来。
Lambda 的主要特点就是需要我们写的东西很少,但是构建 Lambda 的过程是需要思考的,以前看到一个接口或者一个抽象类,我们第一反应是 new Function(){...}
,但是有了 Lambda 以后我们需要重新思考,以便于我们代码更加便于阅读和减少代码的书写。
Lambda 的标准形式:
书中通过构建一个 Comparator 对象来给我们举例说明如果替换原来的匿名内部类写法。 我们都知道实现一个 Comparator 的方法:
Comparator<Apple> byWeight = new Comparator<Apple>() {
public int compare(Apple a1, Apple a2) {
return a1.getWeight().compareTo(a2.getWeight());
}
};
我们可以使用 Comparator 的方法为:
Comparator<Apple> byWeight = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
这看起来并没有难度,阅读起来是 我有两个苹果对象,拿参数 a1,a2 作为比较对象,第一个苹果的重量和第二个苹果的重量比较,并返回比较结果(0代表相同,大于0的数代表a1重量大于a2重量,小于0则相反)。
请注意你基本上只传递了比较两个苹果重量所真 正需要的代码。看起来就像是只传递了compare方法的主体。
书中给出了上述解释,之后给出语法通用表示:
- (parameters) -> expression 即 (参数)-> 方法实现 适合单个语句
- (parameters) -> { statements; } (参数)-> {方法实现;} 适合多语句
注意:
- 使用第一种方式不可出现
;
若方法实现即 Lambda 主体如果有多个语句,就必须使用{;}
- 书中
(String s) -> s.length()
这个例子的后边说了一句话当时对我造成了不少困惑:
第一个Lambda表达式具有一个String类型的参 数并返回一个 int 。 Lambda没有 return 语句, 因为已经隐含了return
- 这说明的意思说 如果是第一种表达方式或者第二种表达方式的返回void 都属于 return 类型可以推敲的时候,可以不写 return ,并不能说 Lambda 没有 return。
哪里可以使用 Lambda
Lambda表达式允许你直接以内联的形式为函数式接口的抽象方法提供实现,并把 整个表达式作为函数式接口的实例 (具体说来,是函数式接口一个具体实现 的实例)。你用匿名内部类也可以完成同样的事情,只不过比较笨拙:需要提供一个实现,然后 再直接内联将它实例化。
这里有两个概念:
- 函数式接口:函数式接口就是只定义一个抽象方法的接口
- Lambda 是函数式接口的抽象方法的实现,但它可以作为整个接口的一个具体实例。
厉害了我的哥,第二点难理解,但这又恰恰是读懂 Lambda 表达式的必须需要理解的地方。
举一个栗子:
// 需要调用函数式接口的方法
public static void process(Runnable r) {
r.run();
}
// 之前的调用方法
Runnable r1 = new Runnable() {
public void run() {
System.out.println("Hello World 2");
}
};
process(r1);
// Lambda 表达方法
process(() -> System.out.println("Hello World 3"));
相信如果有之前实现的方法作比较,我们会很好明白,但是如果你挡住上边的实现,去看下边的 lambda 我相信好多人跟我一样懵逼。这是什么鬼? 虽然我知道结果会是 Hello World 3 ,但是这究竟是什么?
思路是这样的:
- process 方法只接受 Runnable 的具体实现类
- Lambda 可以作为函数式接口的一个实现实例
-
() -> System.out.println("Hello World 3")
应该就是 Runnable 的一个具体实现。 - Lambda 是函数式接口的抽象方法的实现,也就是这句话同时也是 run() 方法的具体实现,run方法不需要参数,所以箭头前边为一个空的 ()。() -> void代表 了参数列表为空,且返回void的函数。
这充分说明 Lambda 写得少想得多的特点。但是我们也发现了他的一个优点就是「简单易读」,我们看到这句话第一个反应就是:它将打印出 Hello World 3 。
方法签名
Java 方法签名 :java 的方法签名由 全类名.方法名(形参数据类型列表)返回值数据类型 决定,在方法存在重载的时候,方法返回值没有什么意义,是由方法名和参数列表决定的。
Lambda 表达式的签名:函数式接口的抽象方法的签名基本上就是Lambda表达式的签名。
书中举了个错误的例子:ApplePredicate<Apple> p = (Apple a) -> a.getWeight();
因为之前我们定义的时候:
public interface ApplePredicate{ boolean test (Apple apple); }
我们可以看到,函数式接口 ApplePredicate test 的方法签名中,返回值为 boolean 但是错误代码中返回了int值,所以方法签名不同。
动手实现一个 Lambda 付诸实践
动手练习:将下列代买转化为 Lambda 实现:
public static String proccessFile() throws IOException {
try (BufferedReader br = new BufferedReader((new FileReader("data.txt")))) {
return br.readLine();
}
}
- 行为参数化
实际操作中我们可能需要对文件进行不同的操作,比如读取两行,读取最后一行。所以我们要将proccessFile ()
方法 对文件进行不同操作的行为,进行参数化。
我们期待的结果是 通过 processFile 拿到文件的读取结果,具体怎么读取应该由接口的抽象方法具体实现,所以 processFile 的行为就是如何操作文件,我们需要将他参数化。 - 使用函数接口来传递行为
为了之后我们能够使用 Lambda 来实现这个功能,我们需要创建一个 函数式接口。对于初学者来说这点应该是最难得,我们应该如何创建这个函数式接口。
- 明确任务条件: 读取文件需要一个文件的读取流 这里假定是字符流 BufferReader
- 明确任务结果: 返回读取的结果,应该是一个 String
即 BufferReader -> String 的过程
interface BufferedReaderProcessor {
String process(BufferedReader bufferedReader);
}
- 执行一个行为
执行这个行为将需要用到processFile
方法了,该方法参数为函数式接口BufferedReaderProcessor
,用该接口的是实例来进行文件操作,具体怎么操作由该实例决定:
public static String processFile(BufferedReaderProcessor p) throws IOException {
try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
return p.process(br);
}
}
- 改为 Lambda 形式完成 process 的具体实现
通过之前的学习,我们知道了一个重要的概念就是 Lambda 可以作为,Lambda 是函数式接口的抽象方法的实现,但它可以作为整个接口的一个具体实例。。
- 需要实现这个接口的具体方法
- 整体作为实例传递给 processFile
假设我们现在需要完成一读取文章前两行的操作:
- (parameters) -> expression
(BufferReader br) -> br.readLine()+br.readLine();
- (parameters) -> { statements; }
String readline = proccessFile((BufferedReader br) -> {
//readLine 方法会有警告 ,之后会学习如何处理 lambda 中的警告 这里重点在于 lambda 的实现。
String line1 = br.readLine();
String line2 = br.readLine();
return line1 + line2;
});
类型检查
Lambda 可以作为函数式接口生成一个实例。然而,Lambda 表达式本身并不包含它在实现哪个函数式接口的信息
正如我们文章开头提到的那样,Lambda 本身并没有什么意义,只有结合上下文,更通俗的说是等号左边的内容,才是我们最终想要的 函数式接口具体实现 对象。
Lambda的类型是从使用Lambda的上下文推断出来的。
这里的上下文包括: 方法签名(参数,返回值),以及使用这个 Lambda 的方法的参数类型。
List<Apple> heavierThan150g = filter(inventory, (Apple a) -> a.getWeight() > 150);
这个例子还是延续第一篇中的那个筛选苹果的例子,书中给出了很好的解读这里就不多赘述直接上图:
看到这里我只能说这本书翻译的太好了。
依次类推我们看地方代码的时候如果它使用了 Lambda 方式书写代码,我们可以通过以下方式来找到这个 Lambda 的含义:
- 找到使用 Lambda 函数式参数方法的方法定义。
- 查看Lambda 所需要实例化的抽象接口的内唯一的一个抽象方法的方法签名(这里是 T -> boolen)
- 检查 Lambda 表达式是否满足这个抽象方法的方法签名即可。
聪明的人可能发现了猫腻,我们可能好多的函数式接口的抽象方法的方法签名都是 T -> boolen,那么 Lambda 的使用会不会出现问题 ?
事实上,同一个 Lambda 表达式,如果 Lambda 表达式的方法签名 = 函数式接口的抽象方法接口 那么这个这个 Lambda 就是有效的。
这就需要在我们书写或者阅读的时候必须结合上下文,而 JVM 需要做的事情更多,但具体原理并不影响我们使用,所以就先不探讨。
类型推断
之前说过 Lambda 表达式还可以继续简单化代码,刚才我们学习了类型检查,相同的 Lambda 表达式可以匹配不同的函数接口,JVM 可以根据我们使用 Lambda 表达式的上下文来决定匹配什么样的函数接口来接收此表达式。
如果这个 Lambda 的目标类型是可以推断出来的,而参数类型也只有一种的时候,我们可以省略参数的类型:
List<Apple> greenApples = filter(inventory, a -> "green".equals(a.getColor()));
Lambda表达式有多个参数,代码可读性的好处就更为明显,以下两个方式是等价的。
Comparator<Apple> c = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
Comparator<Apple> c = (a1, a2) -> a1.getWeight().compareTo(a2.getWeight());
书中也给了我们提醒,有的时候没有类型推断的 Lambda 表达式更易读,有时候去掉看起来更好。
需要我们自己去选择。
至此我们已经成功完成了一次lambda 的实践过程。通过学习我的体会就是想要 lambda 的学习还是需要多加练习。否则总会处在放下课本就忘得状态。至此我们学习完了,课本的3.3章的内容。而之后将会介绍一个新的概念叫「方法引用」,它将会使代码更加简洁,但是同时也带来了更大的挑战,一起来加油吧。