本文是对官方文档的不完全翻译,如有错误请指正。
Lambda 表达式
匿名类的实现非常简单,例如只包含一个方法的接口,在这些情况下,您通常会尝试将函数作为参数传递给另一个方法,例如当有人单击按钮时应采取的操作。 Lambda 表达式使您能够做到这一点,将函数视为方法参数,或将代码视为数据。
对于只有一个方法的类,即使是匿名类也显得有些多余和繁琐。 Lambda 表达式让您可以更紧凑地表达单方法类的实例。
文章目录
Lambda 表达式的理想用例
假设您正在创建一个社交网络应用程序。您希望创建一个功能,使管理员能够对满足特定条件的社交网络应用程序的成员执行任何类型的操作,例如发送消息。下表详细描述了此用例:
字段 | 说明 |
---|---|
名称 | 对选定成员执行操作 |
动作发起人 | 管理员 |
前提条件 | 管理员已登录系统 |
后置条件 | 仅对符合指定条件的成员执行操作 |
主要成功场景 | 管理员指定执行特定操作的成员标准。管理员指定对这些选定成员执行的操作。管理员选择提交按钮。系统查找所有符合指定条件的成员。系统对所有匹配的成员执行指定的操作。 |
扩展 | 管理员可以选择在指定要执行的操作之前或在选择提交按钮之前预览符合指定条件的成员 |
发生频率 | 一天中有很多次 |
假设此社交网络应用程序的成员由以下 Person 类表示:
public class Person {
public enum Sex {
MALE, FEMALE
}
String name;
LocalDate birthday;
Sex gender;
String emailAddress;
public int getAge() {
// ...
}
public void printPerson() {
// ...
}
}
假设您的社交网络应用程序的成员存储在 List 实例中。
本节从对此用例的简单方法开始。它使用本地和匿名类改进了这种方法,然后使用 lambda 表达式以一种高效而简洁的方法结束。在示例 RosterTest 中找到本节中描述的代码摘录。
方法 1:创建搜索匹配一个特征的成员的方法
一种简单的方法是创建几种方法;每种方法都会搜索与某个特征(例如性别或年龄)相匹配的成员。以下方法打印超过指定年龄的成员:
public static void printPersonsOlderThan(List<Person> roster, int age) {
for (Person p : roster) {
if (p.getAge() >= age) {
p.printPerson();
}
}
}
注意:列表是有序集合。集合是将多个元素组合成一个单元的对象。集合用于存储、检索、操作和通信聚合数据。有关集合的更多信息,请参阅集合跟踪。
这种方法可能会使您的应用程序变得脆弱,这是由于引入更新(例如更新的数据类型)而导致应用程序无法运行。假设您升级应用程序并更改 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();
}
}
}
此方法通过调用 tester.test 方法检查 List 参数 roster 中包含的每个 Person 实例是否满足 CheckPerson 参数 tester 中指定的搜索条件。如果 tester.test 方法返回真值,则在 Person 实例上调用 printPersons 方法。
要指定搜索条件,请实现 CheckPerson 接口:
interface CheckPerson {
boolean test(Person p);
}
以下类通过指定方法 test 的实现来实现 CheckPerson 接口。此方法过滤在美国有资格参加选择性服务的成员:如果其 Person 参数为男性且年龄在 18 到 25 岁之间,则返回真值:
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 表达式,而不是使用匿名类表达式,该表达式在以下方法调用中突出显示:
printPersons(
roster,
(Person p) -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25
);
有关如何定义 lambda 表达式的信息,请参阅 Lambda 表达式的语法。
您可以使用标准函数式接口代替 CheckPerson 接口,这进一步减少了所需的代码量。
方法 6:使用带有 Lambda 表达式的标准函数式接口
重新考虑 CheckPerson 接口:
interface CheckPerson {
boolean test(Person p);
}
这是一个非常简单的接口。它是一个函数式接口,因为它只包含一个抽象方法。此方法接受一个参数并返回一个布尔值。该方法非常简单,因此在您的应用程序中不值得定义它。因此,JDK 定义了几个标准函数式接口,您可以在包 java.util.function 中找到这些接口。
例如,您可以使用 Predicate 接口代替 CheckPerson。此接口包含方法 boolean test(T t):
interface Predicate<T> {
boolean test(T t);
}
接口 Predicate 是通用接口的一个示例。 泛型在尖括号 (<>) 中指定一个或多个类型参数。此接口仅包含一个类型参数 T。当您使用实际类型参数声明或实例化泛型类型时,您就有了参数化类型。例如,参数化类型 Predicate 如下:
interface Predicate<Person> {
boolean test(Person t);
}
此参数化类型包含一个与 CheckPerson.boolean test(Person p) 具有相同返回类型和参数的方法。因此,您可以使用 Predicate 代替 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
);
方法 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 参数测试器中指定的条件。如果 Person 实例确实满足 tester 指定的条件,则在 Person 实例上调用 printPerson 方法。
除了调用 printPerson 方法,您还可以指定不同的操作来对那些满足 tester 指定标准的 Person 实例执行。您可以使用 lambda 表达式指定此操作。假设您想要一个类似于 printPerson 的 lambda 表达式,它接受一个参数(Person 类型的对象)并返回 void。请记住,要使用 lambda 表达式,您需要实现一个函数式接口。在这种情况下,您需要一个包含抽象方法的函数式接口,该方法可以接受一个 Person 类型的参数并返回 void。 Consumer 接口包含方法 void accept(T t),它具有这些特性。以下方法将调用 p.printPerson() 替换为调用方法 accept 的 Consumer 的实例:
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指定的数据,然后对其执行参数块指定的操作:
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);
}
}
}
以下方法从名册中包含有资格参加选择性服务的每个成员中检索电子邮件地址,然后将其打印出来:
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 表达式,用于指定哪些成员有资格使用选择性服务。
- 遍历过滤后的由Function类型的mapper指定的对象的值。在此示例中,Function的 对象是一个返回成员电子邮件地址的 lambda 表达式。
- 对遍历的每个对象执行由Consumer类型的block指定操作。在此示例中,Consumer 的对象是一个打印字符串的 lambda 表达式,该字符串是 Function 对象返回的电子邮件地址。
您可以将这些操作中的每一个替换为聚合操作。
方法 9:使用接受 Lambda 表达式作为参数的聚合操作
以下示例使用聚合操作来打印集合中符合选择性服务资格的成员的电子邮件地址:
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 stream() |
过滤匹配Predicate的对象 | Stream filter(Predicate<? super T> predicate) |
将对象映射到 Function 对象指定的另一个值 | Stream map |
(Function<? super T,? extends R> mapper) | |
执行Consumer对象指定的操作 | void forEach(Consumer<? super T> action) |
filter、map 和 forEach 是聚合操作。聚合操作处理来自流的元素,而不是直接来自集合(这就是本例中调用的第一个方法是流的原因)。流(Stream)是一系列元素。与集合不同,它不是存储元素的数据结构。相反,流通过管道(Pipline)从源(例如集合)携带值。管道是一系列流操作,在本例中为 filter-map-forEach。此外,聚合操作通常接受 lambda 表达式作为参数,使您能够自定义它们的行为方式。
GUI 应用程序中的 Lambda 表达式
要处理图形用户界面 (GUI) 应用程序中的事件,例如键盘操作、鼠标操作和滚动操作,您通常会创建事件处理程序(event handlers),这通常涉及实现特定接口。通常,事件处理程序接口(event handler interfaces)是函数式接口;他们往往只有一个方法。
例如把 JavaFX 示例 HelloWorld.java中的匿名类替换为 lambda 表达式:
public class HelloWorld extends Application {
@Override
public void start(Stage primaryStage) {
Button btn = new Button();
btn.setText("Say 'Hello World'");
btn.setOnAction(new EventHandler<ActionEvent>() {
@Override
public void handle(ActionEvent event) {
System.out.println("Hello World!");
}
});
StackPane root = new StackPane();
root.getChildren().add(btn);
Scene scene = new Scene(root, 300, 250);
primaryStage.setTitle("Hello World!");
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
方法调用 btn.setOnAction 指定当您选择由 btn 对象表示的按钮时会发生什么。此方法需要 EventHandler 类型的对象。 EventHandler 接口只包含一个方法,void handle(T event)。该接口是一个函数式接口,因此您可以使用以下突出显示的 lambda 表达式来替换它:
btn.setOnAction(
event -> System.out.println("Hello World!")
);
Lambda 表达式的语法
一个 lambda 表达式由以下三部分组成:
-
括号中由逗号分隔的形式参数列表。
printPersons( roster, (Person p) -> p.getGender() == Person.Sex.MALE && p.getAge() >= 18 && p.getAge() <= 25 );
注意:您可以省略 lambda 表达式中参数的数据类型。此外,如果只有一个参数,您可以省略括号。例如,以下 lambda 表达式也是合法的:
p -> p.getGender() == Person.Sex.MALE && p.getAge() >= 18 && p.getAge() <= 25
-
箭头标记,->
-
主体,由单个表达式或语句块组成。表达式可以省略大括号,表达式的值会被自动返回;语句块必须写在大括号中,void方法调用可以省略大括号。
此示例使用以下表达式,lambda中的表达式可以省略大括号{}:
p -> p.getGender() == Person.Sex.MALE && p.getAge() >= 18 && p.getAge() <= 25
如果您指定单个表达式,则 Java 运行时评估该表达式,然后返回其值。或者,您可以使用 return 语句,return 语句不是表达式;在 lambda 表达式中,您必须将语句括在大括号 ({}) 中:
p -> { return p.getGender() == Person.Sex.MALE && p.getAge() >= 18 && p.getAge() <= 25; }
但是,您不必将 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 对两个整数操作数执行数学运算。操作本身由 IntegerMath 的一个实例指定。该示例使用 lambda 表达式定义了两个操作,加法和减法。该示例打印以下内容:
40 + 2 = 42 20 - 10 = 10
访问封闭范围的局部变量
与局部和匿名类一样,lambda 表达式可以捕获变量;它们对封闭范围的局部变量具有相同的访问权限。但是,与局部和匿名类不同,lambda 表达式没有任何阴影(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) { int z = 2; Consumer<Integer> myConsumer = (y) -> { // The following statement causes the compiler to generate // the error "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 or effectively final)局部变量和参数。在这个例子中,变量 z 实际上是最终的;它的值在初始化后永远不会改变。但是,假设您在 lambda 表达式 myConsumer 中添加以下赋值语句:
Consumer<Integer> myConsumer = (y) -> { z = 99; // ... }
由于这个赋值语句,变量 z 不再是有效的 final 了。结果,Java 编译器生成类似于“在封闭范围中定义的局部变量 z 必须是最终或有效最终”的错误消息。
目标类型
如何确定 lambda 表达式的类型?回想一下选择年龄在 18 到 25 岁之间的男性成员的 lambda 表达式:
p -> p.getGender() == Person.Sex.MALE && p.getAge() >= 18 && p.getAge() <= 25
此 lambda 表达式用于以下两种方法:
public static void printPersons(List<Person> roster, CheckPerson tester)
用例中的方法三public void printPersonsWithPredicate(List<Person> roster, Predicate<Person> tester)
用例中的方法六
当 Java 运行时调用方法 printPersons 时,它需要 CheckPerson 数据类型,因此 lambda 表达式属于 CheckPerson 类型。但是,当 Java 运行时调用方法 printPersonsWithPredicate 时,它需要 Predicate 数据类型,因此 lambda 表达式属于 Predicate 类型。这些方法所期望的数据类型称为目标类型。为了确定 lambda 表达式的类型,Java编译器使用找到lambda表达式的上下文或情况的目标类型。因此,您只能在 Java 编译器可以确定目标类型的情况下使用 lambda 表达式:
- Variable declarations
- Assignments
- Return statements
- Array initializers
- Method or constructor arguments
- Lambda expression bodies
- Conditional expressions, ?:
- Cast expressions
目标类型和方法参数
对于方法参数,Java 编译器使用另外两个语言特性来确定目标类型:重载解析和类型参数推断。
考虑以下两个功能接口(
[java.lang.Runnable](https://docs.oracle.com/javase/8/docs/api/java/lang/Runnable.html)
and[java.util.concurrent.Callable<V>](https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/Callable.html)
):public interface Runnable { void run(); } public interface Callable<V> { V call(); }
Runnable.run 方法不会返回值,而 Callable.call 有返回值。
假设您已按如下方式重载方法invoke
void invoke(Runnable r) { r.run(); } <T> T invoke(Callable<T> c) { return c.call(); }
下面的语句会调用哪个方法?
String s = invoke(() -> "done");
方法 invoke(Callable) 将被调用,因为该方法返回一个值;方法 invoke(Runnable) 没有返回值。在这种情况下,lambda 表达式 () -> “done” 的类型是 Callable。
序列化
如果 lambda 表达式的目标类型及其捕获的参数是可序列化的,则可以序列化它。但是,与内部类一样,强烈建议不要对 lambda 表达式进行序列化。