Lambda 表达式
原文: https://docs.oracle.com/javase/tutorial/java/javaOO/lambdaexpressions.html
匿名类的一个问题是,如果匿名类的实现非常简单,例如只包含一个方法的接口,那么匿名类的语法可能看起来不实用且不清楚。在这些情况下,您通常会尝试将功能作为参数传递给另一个方法,例如当有人单击按钮时应采取的操作。 Lambda 表达式使您可以执行此操作,将功能视为方法参数,或将代码视为数据。
上一节匿名类向您展示了如何在不给它命名的情况下实现基类。虽然这通常比命名类更简洁,但对于只有一个方法的类,即使是匿名类也似乎有点过分和繁琐。 Lambda 表达式允许您更紧凑地表达单方法类的实例。
本节包括以下主题:
假设您正在创建社交网络应用程序。您希望创建一项功能,使管理员能够对满足特定条件的社交网络应用程序成员执行任何类型的操作,例如发送消息。下表详细描述了此用例:
字段 | 描述 |
---|---|
名称 | 对选定的成员执行操作 |
主要演员 | 管理员 |
前提条件 | 管理员已登录系统。 |
后置条件 | 仅对符合指定条件的成员执行操作。 |
主要成功案例 |
- 管理员指定执行特定操作的成员的标准。
- 管理员指定要对这些选定成员执行的操作。
- 管理员选择提交按钮。
- 系统查找符合指定条件的所有成员。
系统对所有匹配成员执行指定的操作。
| | 扩展 | 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
中找到本节中描述的代码摘录。
一种简单的方法是创建几种方法;每种方法都会搜索与一个特征匹配的成员,例如性别或年龄。以下方法打印超过指定年龄的成员:
public static void printPersonsOlderThan(List<Person> roster, int age) {
for (Person p : roster) {
if (p.getAge() >= age) {
p.printPerson();
}
}
}
注: List
是有序 Collection
。集合是一个将多个元素组合成一个单元的对象。集合用于存储,检索,操作和传递聚合数据。有关集合的更多信息,请参阅集合跟踪。
这种方法可能会使您的应用程序变得脆弱,这是由于引入更新(例如更新的数据类型)而导致应用程序无法工作的可能性。假设您升级应用程序并更改Person
类的结构,使其包含不同的成员变量;也许该类记录和测量年龄与不同的数据类型或算法。您必须重写大量 API 以适应此更改。此外,这种方法是不必要的限制;例如,如果您想要打印年龄小于某个年龄的成员,该怎么办?
以下方法比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
更通用,但尝试为每个可能的搜索查询创建单独的方法仍然会导致代码脆弱。您可以改为分隔指定要在其他类中搜索的条件的代码。
以下方法打印与您指定的搜索条件匹配的成员:
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
。
要指定搜索条件,请实现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
实现了一个接口,所以可以使用匿名类而不是本地类,并且无需为每次搜索声明一个新类。
下面调用方法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 表达式而不是匿名类,如下一节中所述。
CheckPerson
接口是函数式接口。函数式接口是仅包含一个抽象方法的任何接口。 (函数式接口可能包含一个或多个默认方法或静态方法。)因为函数式接口只包含一个抽象方法,所以在实现时可以省略该方法的名称。要执行此操作,请使用 _lambda 表达式*,而不是使用匿名类表达式,该表达式在以下方法调用中突出显示:
printPersons(
roster,
(Person p) -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25
);
有关如何定义 lambda 表达式的信息,请参见 Lambda 表达式的语法。
您可以使用标准函数式接口代替接口CheckPerson
,这可以进一步减少所需的代码量。
重新考虑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();
}
}
}
因此,以下方法调用与您在方法 3:在本地类中指定搜索条件代码]中调用printPersons
时相同,以获得有资格获得选择性服务的成员:
printPersonsWithPredicate(
roster,
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25
);
这不是此方法中使用 lambda 表达式的唯一可能位置。以下方法提出了使用 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 表达式,您需要实现一个函数式接口。在这种情况下,您需要一个包含抽象方法的函数式接口,该方法可以接受Person
类型的一个参数并返回 void。 Consumer<T>
接口包含具有这些特征的方法void accept(T t)
。以下方法将调用p.printPerson()
替换为调用方法accept
的Consumer<Person>
实例:
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)
);
重新考虑方法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
对象返回的电子邮件地址。
您可以使用聚合操作替换每个操作。
以下示例使用聚合操作来打印有资格获得选择性服务的集合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 行动 |
聚合操作 |
---|---|
获取对象的来源 | 流< E> **stream** () |
过滤与Predicate 对象匹配的对象 |
流< T> **过滤器**(Predicate<?super T>谓词) |
将对象映射到Function 对象指定的另一个值 |
< R>流< R> **map** (函数<?super T,?extends R> mapper) |
执行Consumer 对象指定的操作 |
void **forEach** (Consumer<?super T>动作) |
操作filter
,map
和forEach
是聚合操作。聚合操作处理流中的元素,而不是直接来自集合(这是此示例中调用的第一个方法是stream
的原因)。流是元素序列。与集合不同,它不是存储元素的数据结构。相反,流通过管道携带来自源(例如集合)的值。管道是流操作的序列,在该示例中是filter
- map
- forEach
。此外,聚合操作通常接受 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 表达式包含以下内容:
括号中用逗号分隔的形式参数列表。
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
对两个整数操作数执行数学运算。操作本身由IntegerMath
的实例指定。该示例使用 lambda 表达式addition
和subtraction
定义了两个操作。该示例打印以下内容:
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) {
// 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 已在方法 methodInFirstLevel(int)中定义”,因为 lambda 表达式不会引入新的作用域级别。因此,您可以直接访问封闭范围的字段,方法和局部变量。例如,lambda 表达式直接访问方法methodInFirstLevel
的参数x
。要访问封闭类中的变量,请使用关键字this
。在此示例中,this.x
指的是成员变量FirstLevel.x
。
但是,与本地和匿名类一样,lambda 表达式只能访问最终或有效最终的封闭块的局部变量和参数。例如,假设您在methodInFirstLevel
定义语句之后立即添加以下赋值语句:
void methodInFirstLevel(int x) {
x = 99;
// ...
}
由于这个赋值语句,变量FirstLevel.x
不再是有效的最终结果。因此,Java 编译器生成类似于“从 lambda 表达式引用的局部变量必须是 final 或者 final final”的错误消息,其中 lambda 表达式myConsumer
尝试访问FirstLevel.x
变量:
System.out.println("x = " + x);
你如何确定 lambda 表达式的类型?回想一下 lambda 表达式,它选择了男性和年龄在 18 到 25 岁之间的成员:
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 表达式:
变量声明
分配
退货声明
数组初始化器
方法或构造器参数
Lambda 表达体
条件表达式,
?:
转换表达式
对于方法参数,Java 编译器使用另外两种语言特性确定目标类型:重载解析和类型参数推断。
考虑以下两个函数式接口( java.lang.Runnable
和 java.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 表达式进行序列化。