Java 教程是为 JDK 8 编写的。本页中描述的示例和实践未利用在后续版本中引入的改进。
匿名类的一个问题是,如果匿名类的实现非常简单,例如只包含一个方法的接口,那么匿名类的语法可能看起来不实用且不清楚。在这些情况下,你通常会尝试将函数作为参数传递给另一个方法,例如传递当有人单击按钮时应采取的操作。Lambda 表达式使你可以执行此操作,将函数视为方法参数,或将代码视为数据。
上一节 匿名类,向你展示了如何在不给它命名的情况下实现某个基类。虽然这通常比命名类更简洁,但对于只有一个方法的类,即使是匿名类也似乎有点过分和繁琐。Lambda 表达式允许你更紧凑地表达单个方法的类的实例。
本节包括以下主题:
假设你正在创建社交网络应用程序。你希望创建一项功能,使管理员能够对满足特定条件的社交网络应用程序成员执行任何类型的操作,例如发送消息。下表详细描述了此用例:
字段 | 描述 |
---|---|
Name | 对选定的成员执行操作 |
主要操作者 | 管理员 |
前提条件 | 管理员已登录系统。 |
后置条件 | 仅对符合指定条件的成员执行操作。 |
主要成功案例 |
|
扩展 |
1a. 管理员可以选择在指定要执行的操作之前或选择 Submit 按钮之前预览符合指定条件的成员。 |
发生频率 | 每天很多次。 |
假设此社交网络应用程序的成员由以下 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(); } } }
Note:List
是有序的 Collection
。collection 是将多个元素组合到一个单元中的对象。集合用于存储,检索,操作和传递聚合数据。有关集合的更多信息,请参阅 Collections 路径。
这种方式可能会使你的应用程序 brittle (脆弱),因为如果引入更新(例如新的数据类型)就可能导致应用程序无法工作。假设你升级应用程序并更改 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(); } } }
此方法检查 List
参数 roster
中包含的每个 Person
实例是否满足由 CheckPerson
类的参数 tester
指定的搜索条件,通过调用方法 tester.test
。如果方法 tester.test
返回 true
值,则在 Person
实例上调用方法 printPersons
。
要指定搜索条件,请实现 CheckPerson
接口:
interface CheckPerson { boolean test(Person p); }
以下类通过指定方法 test
的实现来实现 CheckPerson
接口。此方法筛选符合美国兵役登记制度(Selective Service)的成员:如果 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
接口是 functional interface (函数式接口)。函数式接口是任何只包含一个 抽象方法 的接口。(函数式接口可能包含一个或多个 default methods (默认方法) 或 static methods (静态方法)。)因为函数式接口只包含一个抽象方法,所以当你实现它时可省略该方法的名称。要执行此操作,请使用 lambda 表达式,而不是使用匿名类表达式,该表达式在以下方法调用中高亮:
printPersons( roster, (Person p) -> p.getGender() == Person.Sex.MALE && p.getAge() >= 18 && p.getAge() <= 25 );
有关如何定义 lambda 表达式的信息,请参阅 Syntax of Lambda Expressions。
你可以使用标准函数式接口代替接口 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>
是泛型接口的示例。(有关泛型的更多信息,请参阅 Generics (Updated) 课程。)泛型类型(如泛型接口)在尖括号(<>
)内指定一个或多个类型形参。此接口仅包含一个类型形参 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 操作 |
聚合操作 |
---|---|
获取对象的源 | 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) |
操作 filter
,map
和 forEach
是 aggregate operations (聚合操作)。聚合操作处理流中的元素,而不是直接来自集合(这是在此示例中调用的第一个方法是 stream
的原因)。stream 是一系列元素。与集合不同,它不是存储元素的数据结构。相反,流通过管道携带来自源(例如集合)的值。pipeline (管道) 是一系列流操作,在此示例中是 filter
- map
- forEach
。此外,聚合操作通常接受 lambda 表达式作为参数,使你可以自定义它们的行为方式。
有关聚合操作的更全面讨论,请参阅 Aggregate Operations 课程。
要处理图形用户接口(GUI)应用程序中的事件,例如键盘操作,鼠标操作和滚动操作,通常会创建事件处理程序,这通常涉及实现特定的接口。通常,事件处理程序接口是函数式接口;他们往往只有一种方法。
在 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>
接口只包含一个方法,void handle(T event)
。此接口是一个函数式接口,因此你可以使用以下高亮的 lambda 表达式来替换它:
btn.setOnAction( event -> System.out.println("Hello World!") );
lambda 表达式包含以下内容:
括号中用逗号分隔的形式参数列表。CheckPerson.test
方法包含一个参数 p
,它表示 Person
类的实例。
Note:你可以省略 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 表达式可以 capture variables (捕获变量);它们对封闭范围的局部变量具有相同的访问权限。但是,与局部和匿名类不同,lambda 表达式没有任何遮蔽问题(有关更多信息,请参阅 Shadowing)。Lambda 表达式是词法(lexically)范围的。这意味着它们不会从超类型继承任何名称或引入新级别的范围。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 表达式只能访问 final 或 effectively final 的封闭块的局部变量和参数。例如,假设你在 methodInFirstLevel
定义语句之后立即添加以下赋值语句:
void methodInFirstLevel(int x) { x = 99; // ... }
由于这个赋值语句,变量 FirstLevel.x
不再是 effectively final 的。因此,在 lambda 表达式 myConsumer
尝试访问 FirstLevel.x
变量的地方,Java 编译器生成类似于 "从 lambda 表达式引用的局部变量必须是 final 或者 effectively final" 的错误消息:
System.out.println("x = " + x);
如何确定 lambda 表达式的类型?回想一下 lambda 表达式,它选择了男性且年龄在 18 到 25 岁之间的成员:
p -> p.getGender() == Person.Sex.MALE && p.getAge() >= 18 && p.getAge() <= 25
这个 lambda 表达式用于以下两种方法:
public static void printPersons(List<Person> roster, CheckPerson tester)
in Approach 3: Specify Search Criteria Code in a Local Class
public void printPersonsWithPredicate(List<Person> roster, Predicate<Person> tester)
in Approach 6: Use Standard Functional Interfaces with Lambda Expressions
当 Java 运行时调用方法 printPersons
时,它期望数据类型为 CheckPerson
,因此 lambda 表达式属于此类型。但是,当 Java 运行时调用方法 printPersonsWithPredicate
时,它期望数据类型为 Predicate<Person>
,因此 lambda 表达式属于此类型。这些方法所期望的数据类型称为 target type (目标类型)。要确定 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
(有关重载方法的详细信息,请参阅 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>
。
如果 lambda 表达式的目标类型及其捕获的参数是可序列化的,则可以 serialize (序列化) 一个表达式。但是,与 inner classes 类似,强烈建议不要对 lambda 表达式进行序列化。