Java教程是针对JDK 8编写的。本页面描述的示例和实践不利用后续版本中引入的改进,并且可能使用已不再可用的技术。
请参阅Java语言更改以了解Java SE 9及后续版本中更新的语言功能的摘要。
请参阅JDK发行说明以获取有关所有JDK版本的新功能、增强功能以及已删除或不推荐使用的选项的信息。
匿名类的一个问题是,如果你的匿名类的实现非常简单,比如只包含一个方法的接口,那么匿名类的语法可能会显得笨拙和不清晰。在这些情况下,通常你试图将功能作为参数传递给另一个方法,比如当有人点击一个按钮时应该执行什么操作。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
。一个 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(); } } }
该方法检查 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
实现了一个接口,您可以使用匿名类而不是本地类,从而避免了为每个搜索声明一个新类的需要。
下面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表达式,在以下方法调用中使用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(); } } }
因此,下面的方法调用与您在 方法三:在局部类中指定搜索条件代码 中调用 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
实例上调用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) );
重新考虑方法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
是聚合操作。聚合操作从流中处理元素,而不是直接从集合中处理(这就是为什么在这个示例中首先调用的方法是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表达式是词法作用域的。这意味着它们不会继承任何来自超类型的名称,也不会引入新的作用域级别。下面的示例 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表达式用于以下两个方法:
public static void printPersons(List<Person> roster, CheckPerson tester)
in 第3种方法:在本地类中指定搜索条件代码
public void printPersonsWithPredicate(List<Person> roster, Predicate<Person> tester)
in 第6种方法:使用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表达式进行序列化。