当前教程
编写您的第一个 Lambda 表达式

编写您的第一个 Lambda 表达式

在 2014 年,Java SE 8 引入了 Lambda 表达式的概念。如果您还记得 Java SE 8 发布之前的日子,那么您可能还记得匿名类概念。也许您听说过 Lambda 表达式是另一种更简单的方式,在某些特定情况下,可以编写匿名类的实例。

如果您不记得那些日子,那么您可能听说过或读过关于匿名类的内容,并且可能害怕这种晦涩的语法。

好消息是:您无需通过匿名类来了解如何编写 Lambda 表达式。此外,在许多情况下,由于在 Java 语言中添加了 Lambda,您不再需要匿名类。

编写 Lambda 表达式可以分解为理解三个步骤

  • 识别要编写的 Lambda 表达式的类型
  • 找到要实现的正确方法
  • 实现此方法。

这确实是全部内容。让我们详细了解这三个步骤。

 

识别 Lambda 表达式的类型

在 Java 语言中,所有事物都有类型,并且这种类型在编译时是已知的。因此,始终可以找到 Lambda 表达式的类型。它可能是变量、字段、方法参数或方法的返回类型的类型。

Lambda 表达式的类型有一个限制:它必须是函数式接口。因此,不实现函数式接口的匿名类不能写成 Lambda 表达式。

函数式接口的完整定义有点复杂。您现在需要知道的是,函数式接口是一个只有一个抽象方法的接口。

您应该知道,从 Java SE 8 开始,接口中允许使用具体方法。它们可以是实例方法,在这种情况下,它们被称为默认方法,它们可以是静态方法。这些方法不算,因为它们不是抽象方法。

我是否需要在接口上添加注释 @FunctionalInterface 以使其成为函数式接口?

不需要。此注释是为了帮助您确保您的接口确实是函数式接口。如果您将此注释放在不是函数式接口的类型上,那么编译器将引发错误。

函数式接口的示例

让我们看一些从 JDK API 中获取的示例。我们只是从源代码中删除了注释。

@FunctionalInterface
public interface Runnable {
    public abstract void run();
}

Runnable 接口确实是函数式接口,因为它只有一个抽象方法。 @FunctionalInterface 注释已添加作为帮助程序,但它不是必需的。

@FunctionalInterface
public interface Consumer<T> {

    void accept(T t);

    default Consumer<T> andThen(Consumer<? super T> after) {
        // the body of this method has been removed
    }
}

Consumer<T> 接口也是函数式接口:它有一个抽象方法和一个默认的具体方法,该方法不算。同样,@FunctionalInterface 注释不是必需的。

@FunctionalInterface
public interface Predicate<T> {

    boolean test(T t);

    default Predicate<T> and(Predicate<? super T> other) {
        // the body of this method has been removed
    }

    default Predicate<T> negate() {
        // the body of this method has been removed
    }

    default Predicate<T> or(Predicate<? super T> other) {
        // the body of this method has been removed
    }

    static <T> Predicate<T> isEqual(Object targetRef) {
        // the body of this method has been removed
    }

    static <T> Predicate<T> not(Predicate<? super T> target) {
        // the body of this method has been removed
    }
}

Predicate<T> 接口稍微复杂一些,但它仍然是函数式接口

  • 它有一个抽象方法
  • 它有三个不算的默认方法
  • 它还有两个不算的静态方法。

 

找到要实现的正确方法

此时,您已经确定了需要编写的 Lambda 表达式的类型,好消息是:您已经完成了最困难的部分:剩下的部分非常机械化,更容易完成。

Lambda 表达式是此函数式接口中唯一抽象方法的实现。因此,找到要实现的正确方法只是找到此方法的问题。

您可以花点时间在上一段的三个示例中寻找它。

对于 Runnable 接口,它是

public abstract void run();

对于 Predicate 接口,它是

boolean test(T t);

对于 Consumer<T> 接口,它是

void accept(T t);

 

使用 Lambda 表达式实现正确的方法

编写第一个实现 Predicate<String> 的 Lambda 表达式

现在是最后一步:编写 Lambda 本身。您需要了解的是,您正在编写的 Lambda 表达式是您找到的抽象方法的实现。使用 Lambda 表达式语法,您可以很好地将此实现内联到您的代码中。

此语法由三个元素组成

  • 一个参数块;
  • 一个描绘箭头的 ASCII 艺术小片段:->。请注意,Java 使用简陋箭头 (->) 而不是粗箭头 (=>);
  • 一个代码块,它是方法的主体。

让我们看一些示例。假设您需要一个 Predicate 的实例,该实例对于具有正好 3 个字符的字符字符串返回 true

  1. 您的 Lambda 表达式的类型是 Predicate
  2. 您需要实现的方法是 boolean test(String s)

然后您编写参数块,它只是方法签名的简单复制/粘贴:(String s)

然后您添加一个简陋箭头:->

以及方法的主体。您的结果应该如下所示

Predicate<String> predicate =
    (String s) -> {
        return s.length() == 3;
    };

简化语法

然后可以简化此语法,这要归功于编译器可以猜测许多内容,因此您无需编写它们。

首先,编译器知道您正在实现 Predicate 接口的抽象方法,并且它知道此方法将 String 作为参数。因此 (String s) 可以简化为 (s)。在这种情况下,如果只有一个参数,您甚至可以更进一步,删除括号。参数块然后变为 s。如果您有多个参数或没有参数,则应保留括号。

其次,方法主体中只有一行代码。在这种情况下,您不需要花括号或 return 关键字。

因此,最终语法实际上如下所示

Predicate<String> predicate = s -> s.length() == 3;

这使我们得出第一个最佳实践:保持 Lambda 简短,以便它们只是一行简单易读的代码。

实现 Consumer<String>

在某些时候,人们可能会想走捷径。您会听到开发人员说“消费者接受一个对象,不返回任何内容”。或者“当字符串恰好有三个字符时,谓词为真”。大多数情况下,Lambda 表达式、它实现的抽象方法以及包含此方法的函数式接口之间存在混淆。

但是,由于函数式接口、它的抽象方法以及实现它的 Lambda 表达式紧密相连,因此这种说法实际上很有道理。所以这没问题,只要它不会导致任何歧义。

让我们编写一个使用 String 并打印到 System.out 的 Lambda。语法可以是以下之一

Consumer<String> print = s -> System.out.println(s);

在这里,我们直接编写了 Lambda 表达式的简化版本。

实现 Runnable

实现 Runnable 实际上是编写 void run() 的实现。此参数块为空,因此应使用括号编写。请记住:只有当您有一个参数时,您才能省略括号,这里我们有零个参数。

因此,让我们编写一个告诉我们它正在运行的可运行程序

Runnable runnable = () -> System.out.println("I am running");

 

调用 Lambda 表达式

让我们回到我们之前的 Predicate 示例,并假设此谓词已在方法中定义。您如何使用它来测试给定的字符字符串是否确实长度为 3?

好吧,尽管您使用语法编写了 Lambda,但您需要记住,此 Lambda 是接口 Predicate 的实例。此接口定义了一个名为 test() 的方法,该方法接受一个 String 并返回一个 boolean

让我们在方法中编写它

List<String> retainStringsOfLength3(List<String> strings) {

    Predicate<String> predicate = s -> s.length() == 3;
    List<String> stringsOfLength3 = new ArrayList<>();
    for (String s: strings) {
        if (predicate.test(s)) {
            stringsOfLength3.add(s);
        }
    }
    return stringsOfLength3;
}

请注意您是如何定义谓词的,就像您在前面的示例中所做的那样。由于 Predicate 接口定义了此方法 boolean test(String),因此通过类型为 Predicate 的变量调用在 Predicate 中定义的方法是完全合法的。这乍一看可能令人困惑,因为此谓词变量看起来不像它定义了方法。

请耐心等待,在本教程的后面,您将看到编写此代码的更好方法。

因此,每次您编写 Lambda 时,您都可以调用在该 Lambda 所实现的接口上定义的任何方法。调用抽象方法将调用 Lambda 本身的代码,因为此 Lambda 是该方法的实现。调用默认方法将调用在接口中编写的代码。Lambda 无法覆盖默认方法。

 

捕获局部值

一旦你习惯了它们,编写 lambda 表达式就会变得非常自然。它们在集合框架、流 API 以及 JDK 中的许多其他地方都得到了很好的集成。从 Java SE 8 开始,lambda 表达式无处不在,而且是最好的选择。

使用 lambda 表达式存在一些限制,你可能会遇到需要理解的编译时错误。

让我们考虑以下代码

int calculateTotalPrice(List<Product> products) {

    int totalPrice = 0;
    Consumer<Product> consumer =
        product -> totalPrice += product.getPrice();
    for (Product product: products) {
        consumer.accept(product);
    }
}

即使这段代码看起来不错,尝试编译它也会在使用 totalPriceConsumer 实现中给你以下错误

lambda 表达式中使用的变量应该为 final 或有效 final

原因如下:lambda 表达式不能修改其主体外部定义的变量。它们可以读取这些变量,只要它们是 final 的,即不可变的。这个访问变量的过程称为捕获:lambda 表达式不能捕获变量,它们只能捕获值。final 变量实际上是一个值。

你已经注意到错误消息告诉我们变量可以是final 的,这是 Java 语言中的一个经典概念。它还告诉我们变量可以是有效 final 的。这个概念是在 Java SE 8 中引入的:即使你没有显式地声明一个变量为 final,编译器也可能会为你这样做。如果它看到这个变量是从一个 lambda 表达式中读取的,并且你没有修改它,那么它会为你很好地添加 final 声明。当然,这在编译后的代码中完成,编译器不会修改你的源代码。这样的变量不称为final;它们被称为有效 final 变量。这是一个非常有用的功能。

 

序列化 Lambda 表达式

Lambda 表达式被设计成可以被序列化的。

为什么要序列化 lambda 表达式?嗯,一个 lambda 表达式可以存储在一个字段中,这个字段可以通过构造函数或 setter 方法访问。然后你可能在运行时在你的对象的 state 中有一个 lambda 表达式,而没有意识到它。

因此,为了保持与现有可序列化类的向后兼容性,序列化 lambda 表达式是可能的。

更多学习


最后更新: 2021 年 10 月 26 日


当前教程
编写您的第一个 Lambda 表达式