文档

Java™ 教程
隐藏目录
Lambda 表达式
教程:学习Java语言
课程:类和对象
章节:嵌套类

Lambda表达式

匿名类的一个问题是,如果你的匿名类的实现非常简单,比如只包含一个方法的接口,那么匿名类的语法可能会显得笨拙和不清晰。在这些情况下,通常你试图将功能作为参数传递给另一个方法,比如当有人点击一个按钮时应该执行什么操作。Lambda表达式使您能够以此方式处理功能,将功能作为方法参数,或者将代码作为数据。

前一节 匿名类 向您展示了如何实现一个没有名称的基类。虽然这通常比命名类更简洁,但对于只有一个方法的类来说,即使是匿名类也显得有些冗长和笨重。Lambda表达式可以更简洁地表达单方法类的实例。

本节介绍以下主题:

Lambda表达式的理想用例

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

字段 描述
姓名 对选定成员执行操作
主要角色 管理员
前提条件 管理员已登录系统。
后置条件 只对符合指定条件的成员执行操作。
主要成功场景
  1. 管理员指定要对其执行某个操作的成员的条件。
  2. 管理员指定要对这些选定成员执行的操作。
  3. 管理员选择提交按钮。
  4. 系统查找符合指定条件的所有成员。
  5. 系统对所有符合条件的成员执行指定的操作。
扩展

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: 创建搜索符合单一特征的成员的方法

一种简单的方法是创建多个方法,每个方法搜索符合单一特征(如性别或年龄)的成员。以下方法打印年龄大于指定年龄的成员:

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

注意:一个 List 是一个有序的 Collection。一个 collection 是将多个元素组合成一个单一单元的对象。集合用于存储、检索、操作和传输聚合数据。有关集合的更多信息,请参见 集合 章节。

这种方法有可能使你的应用程序变得脆弱,即由于引入更新(如新的数据类型)导致应用程序无法工作的可能性。假设你升级应用程序并更改 Person 类的结构,使其包含不同的成员变量;也许该类使用不同的数据类型或算法记录和测量年龄。你将不得不重写很多 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 更通用,但为每个可能的搜索查询创建一个单独的方法仍然可能导致脆弱的代码。相反,你可以将指定搜索条件的代码与其他类分开。

方法3:在局部类中指定搜索条件代码

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

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

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

为了指定搜索条件,您需要实现CheckPerson接口:

interface CheckPerson {
    boolean test(Person p);
}

以下类通过为test方法指定实现来实现CheckPerson接口。此方法过滤符合美国选择性服务条件的成员:如果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的结构,您不需要重写方法,但仍然需要额外的代码:一个新的接口和每个搜索的本地类。因为CheckPersonEligibleForSelectiveService实现了一个接口,您可以使用匿名类而不是本地类,从而避免了为每个搜索声明一个新类的需要。

方法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;
        }
    }
);

这种方法减少了所需的代码量,因为您不必为每个要执行的搜索创建一个新类。然而,匿名类的语法较为冗长,考虑到CheckPerson接口只包含一个方法。在这种情况下,您可以使用lambda表达式代替匿名类,如下一节所述。

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

CheckPerson接口是一个函数式接口。函数式接口是指只包含一个抽象方法的接口(函数式接口可以包含一个或多个默认方法静态方法)。由于函数式接口只包含一个抽象方法,您在实现它时可以省略该方法的名称。为了做到这一点,您可以使用lambda表达式,在以下方法调用中使用lambda表达式:

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

请参阅Lambda 表达式的语法以了解如何定义 lambda 表达式。

您可以使用标准的函数式接口来替代 CheckPerson 接口,这样可以进一步减少所需的代码量。

方法六:使用带有 Lambda 表达式的标准函数式接口

重新考虑 CheckPerson 接口:

interface CheckPerson {
    boolean test(Person p);
}

这是一个非常简单的接口。它是一个函数式接口,因为它只包含一个抽象方法。该方法接受一个参数并返回一个 boolean 值。该方法非常简单,可能不值得在您的应用程序中定义一个新的接口。因此,JDK 定义了几个标准的函数式接口,您可以在 java.util.function 包中找到它们。

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

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

Predicate<T> 接口是一个泛型接口的示例。(有关泛型的更多信息,请参阅泛型(更新版)教程。)泛型类型(如泛型接口)在尖括号(<>)内指定一个或多个类型参数。该接口只包含一个类型参数 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();
        }
    }
}

因此,下面的方法调用与您在 方法三:在局部类中指定搜索条件代码 中调用 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实例上调用printPerson方法。

你可以指定一个不同的操作来执行那些满足tester指定条件的Person实例,而不是调用printPerson方法。你可以使用lambda表达式来指定这个操作。假设你想要一个类似于printPerson的lambda表达式,它接受一个参数(Person类型的对象)并返回void。请记住,要使用lambda表达式,你需要实现一个函数式接口。在这种情况下,你需要一个包含一个可以接受Person类型参数并返回void的抽象方法的函数式接口。Consumer<T>接口包含了具有这些特征的void accept(T t)方法。下面的方法用Consumer<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()
);

如果你想要对成员的个人资料进行更多操作而不只是打印出来。假设你想要验证成员的个人资料或者检索他们的联系信息?在这种情况下,你需要一个包含返回值的抽象方法的函数式接口。Function<T,R>接口包含了方法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)
);

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

  1. 从集合source获取对象的源。在此示例中,它从集合roster中获取Person对象的源。注意,集合roster是一个List类型的集合,也是一个Iterable类型的对象。
  2. 过滤与Predicate对象tester匹配的对象。在此示例中,Predicate对象是一个lambda表达式,指定了哪些成员符合选择服务的资格。
  3. 将每个过滤后的对象映射到Function对象mapper指定的值。在此示例中,Function对象是一个lambda表达式,返回一个成员的电子邮件地址。
  4. 根据Consumer对象block指定的操作对每个映射后的对象执行操作。在此示例中,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));

下表将方法processElements执行的每个操作与相应的聚合操作进行了映射:

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

操作filtermapforEach聚合操作。聚合操作从流中处理元素,而不是直接从集合中处理(这就是为什么在这个示例中首先调用的方法是stream的原因)。流是元素的序列。与集合不同,流不是存储元素的数据结构。相反,流通过管道从源(如集合)传递值。管道是一系列的流操作,这个示例中是filter-map-forEach。此外,聚合操作通常接受Lambda表达式作为参数,使您能够自定义它们的行为。

有关聚合操作的更详细讨论,请参阅聚合操作教程。

GUI应用中的Lambda表达式

要处理图形用户界面(GUI)应用程序中的事件,如键盘操作、鼠标操作和滚动操作,通常需要创建事件处理程序,这通常涉及实现特定的接口。通常,事件处理程序接口是功能接口,它们倾向于只有一个方法。

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

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

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

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

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

Lambda表达式的语法

一个lambda表达式由以下部分组成:

请注意,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 方法对两个整数操作数执行数学运算。操作本身由 IntegerMath 的一个实例指定。示例中使用lambda表达式定义了两个操作,additionsubtraction。示例打印如下内容:

40 + 2 = 42
20 - 10 = 10

访问封闭作用域的局部变量

与局部类和匿名类类似,lambda表达式可以捕获变量;它们对封闭作用域的局部变量具有相同的访问权限。然而,与局部类和匿名类不同,lambda表达式没有任何隐藏问题(有关更多信息,请参见 Shadowing)。lambda表达式是词法作用域的。这意味着它们不会继承任何来自超类型的名称,也不会引入新的作用域级别。下面的示例 LambdaScopeTest 演示了这一点:

 
import java.util.function.Consumer;
 
public class LambdaScopeTest {
 
    public int x = 0;
 
    class FirstLevel {
 
        public int x = 1;
        
        void methodInFirstLevel(int x) {

            int z = 2;
             
            Consumer<Integer> myConsumer = (y) -> 
            {
                // 以下语句导致编译器生成错误 "Local variable z defined in an enclosing scope
                // must be final or effectively final" 
                //
                // z = 99;
                
                System.out.println("x = " + x); 
                System.out.println("y = " + y);
                System.out.println("z = " + z);
                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
z = 2
this.x = 1
LambdaScopeTest.this.x = 0

如果您在lambda表达式myConsumer的声明中以参数x替换y,那么编译器会生成一个错误:

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

编译器会生成错误信息"Lambda expression's parameter x cannot redeclare another local variable defined in an enclosing scope",因为lambda表达式不会引入新的作用域层级。因此,您可以直接访问封闭作用域的字段、方法和局部变量。例如,lambda表达式直接访问方法methodInFirstLevel的参数x。要访问封闭类中的变量,请使用关键字this。在此示例中,this.x指的是成员变量FirstLevel.x

但是,与局部类和匿名类一样,lambda表达式只能访问封闭块中的局部变量和参数,这些变量和参数必须是final或者事实上是final的。在此示例中,变量z是事实上是final的;它在初始化后没有改变过。然而,假设您在lambda表达式myConsumer中添加以下赋值语句:

Consumer<Integer> myConsumer = (y) -> {
    z = 99;
    // ...
}

由于这个赋值语句,变量z不再是事实上是final的。结果,Java编译器会生成类似于"Local variable z defined in an enclosing scope must be final or effectively final"的错误信息。

目标类型

如何确定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表达式的类型是这个。这些方法所期望的数据类型称为目标类型。为了确定lambda表达式的类型,Java编译器使用找到lambda表达式的上下文或情境的目标类型。因此,您只能在Java编译器可以确定目标类型的情况下使用lambda表达式:

目标类型和方法参数

对于方法参数,Java编译器通过两种其他语言特性来确定目标类型:重载解析和类型参数推断。

考虑以下两个函数式接口(java.lang.Runnablejava.util.concurrent.Callable<V>):

public interface Runnable {
    void run();
}

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

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

假设你已经重载了以下方法invoke(有关方法重载的更多信息,请参阅定义方法):

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>

序列化

如果lambda表达式的目标类型和捕获的参数都是可序列化的,那么你可以对其进行序列化。然而,与内部类一样,强烈不建议对lambda表达式进行序列化。


上一页: 匿名类
下一页: 方法引用