Lambda Expressions

匿名类的一个问题是,如果匿名类的实现非常简单(例如仅包含一个方法的interface),则匿名类的语法可能看起来笨拙且不清楚。在这些情况下,您通常试图将功能作为参数传递给另一种方法,例如,当某人单击按钮时应采取什么措施。 Lambda 表达式使您能够执行此操作,将功能视为方法参数,或将代码视为数据。

上一节Anonymous Classes展示了如何在不给 Base Class 命名的情况下实现它。尽管这通常比命名类更简洁,但是对于仅具有一种方法的类,即使是匿名类也显得有些繁琐。 Lambda 表达式使您可以更紧凑地表达单方法类的实例。

本节涵盖以下主题:

Lambda 表达式的理想用例

假设您正在创建一个社交网络应用程序。您想创建一个功能,使 管理 员可以对满足特定条件的社交网络应用程序成员执行任何类型的操作,例如发送消息。下表详细描述了该用例:

FieldDescription
Name对所选成员执行操作
Primary ActorAdministrator
Preconditions管理 员登录系统。
Postconditions仅对符合指定条件的成员执行操作。
主要成功方案管理 员指定对其执行特定操作的成员的条件。


管理 员指定要对那些选定成员执行的操作。
管理 员选择“提交”按钮。
系统查找与指定条件匹配的所有成员。
系统对所有匹配成员执行指定的操作。
|扩展| 1a。管理 员可以选择预览符合指定条件的成员,然后再指定要执行的操作或选择“提交”按钮之前。
|发生频率|白天多次。

假设此社交网络应用程序的成员由以下Person类表示:

public class Person {

    public enum Sex {
        MALE, FEMALE
    }

    String name;
    LocalDate birthday;
    Sex gender;
    String emailAddress;

    public int getAge() {
        // ...
    }

    public void printPerson() {
        // ...
    }
}

假设您的社交网络应用程序的成员存储在List<Person>实例中。

本节以对这种用例的幼稚方法开始。它使用本地和匿名类对该方法进行了改进,然后使用 lambda 表达式以一种高效而简洁的方法结束了。在示例RosterTest中找到本节中描述的代码摘录。

方法 1:创建搜索符合一个 Feature 的成员的方法

一种简单的方法是创建几种方法。每种方法都搜索与一个 Feature(例如性别或年龄)相匹配的成员。下面的方法将打印超过指定年龄的成员:

public static void printPersonsOlderThan(List<Person> roster, int age) {
    for (Person p : roster) {
        if (p.getAge() >= age) {
            p.printPerson();
        }
    }
}

注意List是有序的Collection。 * collection *是将多个元素分组为一个单元的对象。集合用于存储,检索,操作和传达聚合数据。有关集合的更多信息,请参见Collections

这种方法可能会使您的应用程序变得“脆弱”,这是由于引入了更新(例如更新的数据类型)而导致应用程序无法运行的可能性。假设您升级了应用程序并更改了Person类的结构,使其包含不同的成员变量;也许该class使用不同的数据类型或算法记录和衡量年龄。您将不得不重写许多 API 来适应此更改。另外,这种方法是不必要的限制。例如,如果要打印小于特定年龄的成员该怎么办?

方法 2:创建更多通用搜索方法

以下方法比printPersonsOlderThan更通用;它打印指定年龄范围内的成员:

public static void printPersonsWithinAgeRange(
    List<Person> roster, int low, int high) {
    for (Person p : roster) {
        if (low <= p.getAge() && p.getAge() < high) {
            p.printPerson();
        }
    }
}

如果要打印指定性别或指定性别和年龄范围的组合,该怎么办?如果您决定更改Person类并添加其他属性(例如关系状态或地理位置)怎么办?尽管此方法比printPersonsOlderThan更通用,但是try为每个可能的搜索查询创建单独的方法仍会导致代码变脆。相反,您可以在其他类中分隔指定要搜索标准的代码。

方法 3:在本地类别中指定搜索条件代码

下面的方法打印与您指定的搜索条件匹配的成员:

public static void printPersons(
    List<Person> roster, CheckPerson tester) {
    for (Person p : roster) {
        if (tester.test(p)) {
            p.printPerson();
        }
    }
}

此方法通过调用方法tester.test来检查List参数roster中包含的每个Person实例是否满足CheckPerson参数tester中指定的搜索条件。如果方法tester.test返回true值,则在Person实例上调用方法printPersons

要指定搜索条件,请实现CheckPersoninterface:

interface CheckPerson {
    boolean test(Person p);
}

下列类通过指定方法test的实现来实现CheckPersoninterface。此方法过滤在美国符合资格参加选择性服务的成员:如果其Person参数是男性且年龄在 18 到 25 之间,则返回true值:

class CheckPersonEligibleForSelectiveService implements CheckPerson {
    public boolean test(Person p) {
        return p.gender == Person.Sex.MALE &&
            p.getAge() >= 18 &&
            p.getAge() <= 25;
    }
}

要使用此类,请创建该类的新实例并调用printPersons方法:

printPersons(
    roster, new CheckPersonEligibleForSelectiveService());

尽管这种方法不那么灵活-如果更改Person的结构,您不必重写方法-您仍然还有其他代码:计划在应用程序中执行的每个搜索都有一个新的interface和一个本地类。由于CheckPersonEligibleForSelectiveService实现了一个interface,因此您可以使用匿名类代替本地类,并且无需为每次搜索声明一个新类。

方法 4:在匿名类中指定搜索条件代码

以下对方法printPersons的调用的一个参数是一个匿名类,该类过滤在美国符合资格参加选择性服务的成员:男性,年龄在 18 至 25 岁之间:

printPersons(
    roster,
    new CheckPerson() {
        public boolean test(Person p) {
            return p.getGender() == Person.Sex.MALE
                && p.getAge() >= 18
                && p.getAge() <= 25;
        }
    }
);

这种方法减少了所需的代码量,因为您不必为要执行的每个搜索创建一个新类。但是,考虑到CheckPersoninterface仅包含一种方法,匿名类的语法非常庞大。在这种情况下,您可以使用 lambda 表达式代替匿名类,如下一节所述。

方法 5:使用 Lambda 表达式指定搜索条件代码

CheckPersoninterface是功能interface。功能interface是仅包含一个abstract method的任何interface。 (一个功能interface可能包含一个或多个default methodsstatic methods。)由于一个功能interface仅包含一个抽象方法,因此在实现该方法时可以省略该方法的名称。为此,您可以使用 lambda 表达式(而不是使用匿名类表达式),该表达式在以下方法调用中突出显示:

printPersons(
    roster,
    (Person p) -> p.getGender() == Person.Sex.MALE
        && p.getAge() >= 18
        && p.getAge() <= 25
);

有关如何定义 lambda 表达式的信息,请参见Lambda 表达式的语法

您可以使用标准功能interface代替CheckPerson,这甚至可以进一步减少所需的代码量。

方法 6:将标准功能interface与 Lambda 表达式一起使用

重新考虑CheckPersoninterface:

interface CheckPerson {
    boolean test(Person p);
}

这是一个非常简单的interface。这是一个功能interface,因为它仅包含一个抽象方法。此方法采用一个参数并返回boolean值。该方法是如此简单,以至于在您的应用程序中定义一个方法可能不值得。因此,JDK 定义了几个标准功能interface,您可以在软件包java.util.function中找到它们。

例如,您可以使用Predicate<T>interface代替CheckPerson。该interface包含方法boolean test(T t)

interface Predicate<T> {
    boolean test(T t);
}

interfacePredicate<T>是通用interface的示例。 (有关泛型的更多信息,请参见Generics (Updated)类。)泛型(例如,泛型interface)在尖括号(<>)中指定一个或多个类型参数。该interface仅包含一个类型参数T。当使用实际类型参数声明或实例化泛型类型时,您将拥有一个参数化类型。例如,参数化类型Predicate<Person>如下:

interface Predicate<Person> {
    boolean test(Person t);
}

此参数化类型包含一个方法,该方法具有与CheckPerson.boolean test(Person p)相同的返回类型和参数。因此,可以使用Predicate<T>代替CheckPerson,如以下方法所示:

public static void printPersonsWithPredicate(
    List<Person> roster, Predicate<Person> tester) {
    for (Person p : roster) {
        if (tester.test(p)) {
            p.printPerson();
        }
    }
}

因此,以下方法调用与您在方法 3:在本地类中指定搜索条件代码中调用printPersons以获取有资格使用选择性服务的成员时相同:

printPersonsWithPredicate(
    roster,
    p -> p.getGender() == Person.Sex.MALE
        && p.getAge() >= 18
        && p.getAge() <= 25
);

这不是使用 lambda 表达式的唯一方法。以下方法建议了使用 lambda 表达式的其他方法。

方法 7:在整个应用程序中使用 Lambda 表达式

重新考虑方法printPersonsWithPredicate,以了解您还可以在何处使用 lambda 表达式:

public static void printPersonsWithPredicate(
    List<Person> roster, Predicate<Person> tester) {
    for (Person p : roster) {
        if (tester.test(p)) {
            p.printPerson();
        }
    }
}

此方法检查List参数roster中包含的每个Person实例是否满足Predicate参数tester中指定的条件。如果Person实例确实满足tester指定的条件,则在Person实例上调用方法printPersron

您可以指定其他方法来对满足tester指定的条件的Person实例执行不同的操作,而不是调用方法printPerson。您可以使用 lambda 表达式指定此操作。假设您想要一个类似于printPerson的 lambda 表达式,该表达式需要一个参数(类型为Person的对象)并返回 void。请记住,要使用 lambda 表达式,您需要实现一个功能interface。在这种情况下,您需要一个包含抽象方法的功能interface,该抽象方法可以采用Person类型的一个参数并返回 void。 Consumer<T>interface包含方法void accept(T t),该方法具有这些 Feature。以下方法用调用方法acceptConsumer<Person>实例替换调用p.printPerson()

public static void processPersons(
    List<Person> roster,
    Predicate<Person> tester,
    Consumer<Person> block) {
        for (Person p : roster) {
            if (tester.test(p)) {
                block.accept(p);
            }
        }
}

因此,以下方法调用与您在方法 3:在本地类中指定搜索条件代码中调用printPersons以获取有资格使用选择性服务的成员时相同。用于打印成员的 lambda 表达式突出显示:

processPersons(
     roster,
     p -> p.getGender() == Person.Sex.MALE
         && p.getAge() >= 18
         && p.getAge() <= 25,
     p -> p.printPerson()
);

如果您想对会员 Profile 进行更 multiprocessing 而不是打印出来,该怎么办。假设您要验证成员的 Profile 或检索他们的联系信息?在这种情况下,您需要一个功能interface,其中包含一个返回值的抽象方法。 Function<T,R>interface包含方法R apply(T t)。以下方法检索由参数mapper指定的数据,然后对由参数block指定的数据执行操作:

public static void processPersonsWithFunction(
    List<Person> roster,
    Predicate<Person> tester,
    Function<Person, String> mapper,
    Consumer<String> block) {
    for (Person p : roster) {
        if (tester.test(p)) {
            String data = mapper.apply(p);
            block.accept(data);
        }
    }
}

以下方法从roster中包含符合选择性服务资格的每个成员中检索电子邮件地址,然后进行打印:

processPersonsWithFunction(
    roster,
    p -> p.getGender() == Person.Sex.MALE
        && p.getAge() >= 18
        && p.getAge() <= 25,
    p -> p.getEmailAddress(),
    email -> System.out.println(email)
);

方法 8:更广泛地使用泛型

重新考虑方法processPersonsWithFunction。以下是其通用版本,该通用版本接受包含任何数据类型的元素的集合作为参数:

public static <X, Y> void processElements(
    Iterable<X> source,
    Predicate<X> tester,
    Function <X, Y> mapper,
    Consumer<Y> block) {
    for (X p : source) {
        if (tester.test(p)) {
            Y data = mapper.apply(p);
            block.accept(data);
        }
    }
}

要打印符合选择服务资格的成员的电子邮件地址,请按以下方式调用processElements方法:

processElements(
    roster,
    p -> p.getGender() == Person.Sex.MALE
        && p.getAge() >= 18
        && p.getAge() <= 25,
    p -> p.getEmailAddress(),
    email -> System.out.println(email)
);

此方法调用执行以下操作:

  • 从集合source获得对象的源。在此示例中,它从集合roster中获得Person个对象的源。注意,集合roster是类型List的集合,也是类型Iterable的对象。

  • 过滤与Predicate对象tester匹配的对象。在此示例中,Predicate对象是 lambda 表达式,用于指定哪些成员符合选择服务的条件。

  • 将每个过滤的对象 Map 到Function对象mapper指定的值。在此的示例Function对象是一个 lambda 表达式,该表达式返回成员的电子邮件地址。

  • Consumer对象block指定的每个 Map 对象执行操作。在此的示例Consumer对象是一个 lambda 表达式,它打印一个字符串,该字符串 是Function对象返回的电子邮件地址。

您可以将这些操作中的每一个替换为聚合操作。

方法 9:使用接受 Lambda 表达式作为参数的聚合操作

下面的示例使用聚合操作来打印集合roster中包含的符合选择性服务资格的那些成员的电子邮件地址:

roster
    .stream()
    .filter(
        p -> p.getGender() == Person.Sex.MALE
            && p.getAge() >= 18
            && p.getAge() <= 25)
    .map(p -> p.getEmailAddress())
    .forEach(email -> System.out.println(email));

下表 Map 了方法processElements所执行的每个操作以及相应的聚合操作:

processElements动作Aggregate Operation
获取对象的来源Stream<E> stream()
过滤与Predicate对象匹配的对象Stream<T> filter(Predicate<? super T> predicate)
将对象 Map 到Function对象指定的另一个值<R> Stream<R> map(Function<? super T,? extends R> mapper)
执行Consumer对象指定的操作void forEach(Consumer<? super T> action)

运算filtermapforEach集合运算。聚合操作处理流中的元素,而不是直接从集合中处理元素(这就是本例中调用的第一个方法是stream的原因)。 * stream *是一系列元素。与集合不同,它不是存储元素的数据结构。取而代之的是,流通过管道携带来自源(如集合)的值。 管道是一系列流操作,在此示例中为filter-map-forEach。此外,聚合操作通常接受 lambda 表达式作为参数,使您能够自定义它们的行为。

有关聚合操作的更详尽讨论,请参见Aggregate Operations节。

GUI 应用程序中的 Lambda 表达式

为了处理图形用户interface(GUI)应用程序中的事件,例如键盘操作,鼠标操作和滚动操作,通常需要创建事件处理程序,这通常涉及实现特定的interface。通常,事件处理程序interface是功能interface。他们往往只有一种方法。

在 JavaFX 示例HelloWorld.java(在上一章节Anonymous Classes中讨论)中,您可以在此语句中用 lambda 表达式替换突出显示的匿名类:

btn.setOnAction(new EventHandler<ActionEvent>() {

            @Override
            public void handle(ActionEvent event) {
                System.out.println("Hello World!");
            }
        });

方法调用btn.setOnAction指定当您选择btn对象表示的按钮时发生的情况。此方法需要一个EventHandler<ActionEvent>类型的对象。 EventHandler<ActionEvent>interface仅包含一种方法void handle(T event)。此interface是功能性interface,因此您可以使用以下突出显示的 lambda 表达式来替换它:

btn.setOnAction(
          event -> System.out.println("Hello World!")
        );

Lambda 表达式的语法

Lambda 表达式包含以下内容:

  • 用括号括起来的形式参数的逗号分隔列表。 CheckPerson.test方法包含一个参数p,该参数表示Person类的实例。

注意 :您可以省略 lambda 表达式中参数的数据类型。此外,如果只有一个参数,则可以省略括号。例如,以下 lambda 表达式也有效:

p -> p.getGender() == Person.Sex.MALE 
    && p.getAge() >= 18
    && p.getAge() <= 25
  • 箭头标记->

  • 主体,由单个表达式或语句块组成。本示例使用以下表达式:

p.getGender() == Person.Sex.MALE 
    && p.getAge() >= 18
    && p.getAge() <= 25

如果指定单个表达式,则 Java 运行时将评估该表达式,然后返回其值。另外,您可以使用 return 语句:

p -> {
    return p.getGender() == Person.Sex.MALE
        && p.getAge() >= 18
        && p.getAge() <= 25;
}

return 语句不是表达式。在 lambda 表达式中,必须将语句括在大括号({})中。但是,您不必在括号中包含 void 方法调用。例如,以下是有效的 lambda 表达式:

email -> System.out.println(email)

注意,lambda 表达式看起来很像方法声明。您可以将 lambda 表达式视为匿名方法,即没有名称的方法。

以下示例Calculator是采用多个形式参数的 lambda 表达式的示例:

public class Calculator {
  
    interface IntegerMath {
        int operation(int a, int b);   
    }
  
    public int operateBinary(int a, int b, IntegerMath op) {
        return op.operation(a, b);
    }
 
    public static void main(String... args) {
    
        Calculator myApp = new Calculator();
        IntegerMath addition = (a, b) -> a + b;
        IntegerMath subtraction = (a, b) -> a - b;
        System.out.println("40 + 2 = " +
            myApp.operateBinary(40, 2, addition));
        System.out.println("20 - 10 = " +
            myApp.operateBinary(20, 10, subtraction));    
    }
}

方法operateBinary对两个整数操作数执行 math 运算。该操作本身由IntegerMath的实例指定。该示例使用 lambda 表达式additionsubtraction定义了两个操作。该示例打印以下内容:

40 + 2 = 42
20 - 10 = 10

访问封闭范围的局部变量

像本地和匿名类一样,lambda 表达式可以capture variables;它们对封闭范围的局部变量具有相同的访问权限。但是,与本地和匿名类不同,lambda 表达式没有任何 shade 问题(有关更多信息,请参见Shadowing)。 Lambda 表达式具有词法范围。这意味着它们不会从超类型继承任何名称,也不会引入新的作用域范围。解释 lambda 表达式中的声明就像在封闭环境中一样。以下示例LambdaScopeTest演示了这一点:

import java.util.function.Consumer;

public class LambdaScopeTest {

    public int x = 0;

    class FirstLevel {

        public int x = 1;

        void methodInFirstLevel(int x) {
            
            // The following statement causes the compiler to generate
            // the error "local variables referenced from a lambda expression
            // must be final or effectively final" in statement A:
            //
            // x = 99;
            
            Consumer<Integer> myConsumer = (y) -> 
            {
                System.out.println("x = " + x); // Statement A
                System.out.println("y = " + y);
                System.out.println("this.x = " + this.x);
                System.out.println("LambdaScopeTest.this.x = " +
                    LambdaScopeTest.this.x);
            };

            myConsumer.accept(x);

        }
    }

    public static void main(String... args) {
        LambdaScopeTest st = new LambdaScopeTest();
        LambdaScopeTest.FirstLevel fl = st.new FirstLevel();
        fl.methodInFirstLevel(23);
    }
}

本示例生成以下输出:

x = 23
y = 23
this.x = 1
LambdaScopeTest.this.x = 0

如果在 lambda 表达式myConsumer的声明中用参数x代替y,则编译器将生成错误:

Consumer<Integer> myConsumer = (x) -> {
    // ...
}

编译器会生成错误“方法 x 中已经定义了变量 x”,因为 lambda 表达式未引入新的作用域范围。因此,您可以直接访问封闭范围的字段,方法和局部变量。例如,lambda 表达式直接访问方法methodInFirstLevel的参数x。要访问封闭类中的变量,请使用关键字this。在此示例中,this.x引用成员变量FirstLevel.x

但是,与本地和匿名类一样,lambda 表达式只能访问final或有效final的封闭块的局部变量和参数。例如,假设您在methodInFirstLevel定义语句之后立即添加以下赋值语句:

void methodInFirstLevel(int x) {
    x = 99;
    // ...
}

由于此赋值语句,变量FirstLevel.x不再有效地变为 final。结果,Java 编译器会生成一条错误消息,类似于“从 lambda 表达式引用的局部变量必须是 final 或有效地是 final”,其中 lambda 表达式myConsumer试图访问FirstLevel.x变量:

System.out.println("x = " + x);

Target Typing

您如何确定 Lambda 表达式的类型?回忆一下选择了男性且年龄在 18 至 25 岁之间的成员的 lambda 表达式:

p -> p.getGender() == Person.Sex.MALE
    && p.getAge() >= 18
    && p.getAge() <= 25

此 lambda 表达式用于以下两种方法:

当 Java 运行时调用方法printPersons时,期望的数据类型为CheckPerson,因此 lambda 表达式就是这种类型。但是,当 Java 运行时调用printPersonsWithPredicate方法时,它期望的是Predicate<Person>数据类型,因此 lambda 表达式就是这种类型。这些方法期望的数据类型称为* target type *。为了确定 lambda 表达式的类型,Java 编译器使用找到 lambda 表达式的上下文或情况的目标类型。因此,您只能在 Java 编译器可以确定目标类型的情况下使用 lambda 表达式:

  • Variable declarations

  • Assignments

  • Return statements

  • Array initializers

  • 方法或构造函数参数

  • Lambda 表达体

  • 条件表达式,?:

  • Cast expressions

目标类型和方法参数

对于方法参数,Java 编译器使用其他两种语言功能确定目标类型:重载解析和类型参数推断。

考虑以下两个功能interface(java.lang.Runnablejava.util.concurrent.Callable<V>):

public interface Runnable {
    void run();
}

public interface Callable<V> {
    V call();
}

方法Runnable.run不返回值,而方法Callable<V>.call则不返回值。

假设您已按如下方式重载了方法invoke(有关重载方法的更多信息,请参见Defining Methods):

void invoke(Runnable r) {
    r.run();
}

<T> T invoke(Callable<T> c) {
    return c.call();
}

以下语句将调用哪种方法?

String s = invoke(() -> "done");

方法invoke(Callable<T>)将被调用,因为该方法返回一个值;方法invoke(Runnable)没有。在这种情况下,lambda 表达式() -> "done"的类型为Callable<T>

Serialization

如果 lambda 表达式的目标类型和catch的参数可序列化,则可以serialize。但是,与inner classes一样,强烈建议不要对 lambda 表达式进行序列化。