Java8 实战学习 — Lambda 表达式

上一章,我们学习了参数化代码的实现方法,这个逻辑的推导对我自己来说还是蛮有意义的,因为这将对我以后的代码编辑产生影响。

这一节我们继续学习,我们将学习 Lambda 表达式的具体使用。

Lambda 概述:

可以把Lambda表达式理解为简洁地表示可传递的匿名函数的一种方式:它没有名称,但它 有参数列表、函数主体、返回类型,可能还有一个可以抛出的异常列表。这个定义够大的,让我 们慢慢道来。

Lambda 的主要特点就是需要我们写的东西很少,但是构建 Lambda 的过程是需要思考的,以前看到一个接口或者一个抽象类,我们第一反应是 new Function(){...} ,但是有了 Lambda 以后我们需要重新思考,以便于我们代码更加便于阅读和减少代码的书写。


Lambda 的标准形式:

Java 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方法的主体。

书中给出了上述解释,之后给出语法通用表示:

  1. (parameters) -> expression 即 (参数)-> 方法实现 适合单个语句
  2. (parameters) -> { statements; } (参数)-> {方法实现;} 适合多语句

注意:

  1. 使用第一种方式不可出现 若方法实现即 Lambda 主体如果有多个语句,就必须使用{;}
  2. 书中 (String s) -> s.length() 这个例子的后边说了一句话当时对我造成了不少困惑:

第一个Lambda表达式具有一个String类型的参 数并返回一个 int 。 Lambda没有 return 语句, 因为已经隐含了return

  1. 这说明的意思说 如果是第一种表达方式或者第二种表达方式的返回void 都属于 return 类型可以推敲的时候,可以不写 return ,并不能说 Lambda 没有 return。

哪里可以使用 Lambda

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

这里有两个概念:

  1. 函数式接口:函数式接口就是只定义一个抽象方法的接口
  2. 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 ,但是这究竟是什么?

思路是这样的:

  1. process 方法只接受 Runnable 的具体实现类
  2. Lambda 可以作为函数式接口的一个实现实例
  3. () -> System.out.println("Hello World 3") 应该就是 Runnable 的一个具体实现。
  4. 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();
        }
    }
  1. 行为参数化
    实际操作中我们可能需要对文件进行不同的操作,比如读取两行,读取最后一行。所以我们要将 proccessFile () 方法 对文件进行不同操作的行为,进行参数化
    我们期待的结果是 通过 processFile 拿到文件的读取结果,具体怎么读取应该由接口的抽象方法具体实现,所以 processFile 的行为就是如何操作文件,我们需要将他参数化。
  2. 使用函数接口来传递行为
    为了之后我们能够使用 Lambda 来实现这个功能,我们需要创建一个 函数式接口。对于初学者来说这点应该是最难得,我们应该如何创建这个函数式接口。
  1. 明确任务条件: 读取文件需要一个文件的读取流 这里假定是字符流 BufferReader
  2. 明确任务结果: 返回读取的结果,应该是一个 String

即 BufferReader -> String 的过程

interface BufferedReaderProcessor {
        String process(BufferedReader bufferedReader);
}
  1. 执行一个行为
    执行这个行为将需要用到 processFile 方法了,该方法参数为函数式接口 BufferedReaderProcessor ,用该接口的是实例来进行文件操作,具体怎么操作由该实例决定:
public static String processFile(BufferedReaderProcessor p) throws IOException {
    try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
        return p.process(br);
    }
}
  1. 改为 Lambda 形式完成 process 的具体实现
    通过之前的学习,我们知道了一个重要的概念就是 Lambda 可以作为,Lambda 是函数式接口的抽象方法的实现,但它可以作为整个接口的一个具体实例。
  1. 需要实现这个接口的具体方法
  2. 整体作为实例传递给 processFile

假设我们现在需要完成一读取文章前两行的操作:

  1. (parameters) -> expression
(BufferReader br) -> br.readLine()+br.readLine();
  1. (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);

这个例子还是延续第一篇中的那个筛选苹果的例子,书中给出了很好的解读这里就不多赘述直接上图:

Java lambda 转换的目标类型必须是接口_Apple_02

看到这里我只能说这本书翻译的太好了。

依次类推我们看地方代码的时候如果它使用了 Lambda 方式书写代码,我们可以通过以下方式来找到这个 Lambda 的含义:

  1. 找到使用 Lambda 函数式参数方法的方法定义。
  2. 查看Lambda 所需要实例化的抽象接口的内唯一的一个抽象方法的方法签名(这里是 T -> boolen)
  3. 检查 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章的内容。而之后将会介绍一个新的概念叫「方法引用」,它将会使代码更加简洁,但是同时也带来了更大的挑战,一起来加油吧。