文章目录
- 简介
- Lambda 表达式的语法
- 语法规则
- lambda 表达式示例
- Lambda 表达式的理想用例
- 方法一:使用 Lambda 表达式指定搜索条件代码
- 方法二:使用Java自带的接口实现实现上述功能
- 方法三:使用聚合操作改写上面的例子
- Lambda表达式的高级用法
- 访问封闭范围的局部变量
- 目标类型
- 序列化
简介
Lambda 表达式就是将函数作为参数传递给另一个方法。对于只有一个方法的类,即使是匿名类也显得有些多余和繁琐。 Lambda 表达式可以更紧凑地表达单方法类的实例。
Lambda 表达式的语法
语法规则
一个 lambda 表达式由以下三部分组成:1、形参列表。2、箭头标记。3、主体。下面分别介绍这三部分:
- 括号中由逗号分隔的形式参数列表。 例如下面的例子筛选年龄在18-15岁之间的男性,形参列表为(Person p)
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 表达式示例
以下示例 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 表达式的理想用例
以社交网络应用程序为例,您需要实现一个功能,使管理员能够对满足特定条件的成员执行的操作,例如发送消息。下表详细描述了此用例:
字段 | 说明 |
名称 | 对选定成员执行操作 |
动作发起人 | 管理员 |
前提条件 | 管理员已登录系统 |
后置条件 | 仅对符合指定条件的成员执行操作 |
主要成功场景 | 管理员指定执行特定操作的成员标准。管理员指定对这些选定成员执行的操作。管理员选择提交按钮。系统查找所有符合指定条件的成员。系统对所有匹配的成员执行指定的操作。 |
扩展 | 管理员可以选择在指定要执行的操作之前或在选择提交按钮之前预览符合指定条件的成员 |
发生频率 | 一天中有很多次 |
假设此社交网络应用程序的成员由以下 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 表达式指定搜索条件代码
假如我们要筛选年龄在18-25岁之间的男性成员。
首先需要一个指定搜索条件的接口:
interface CheckPerson {
boolean test(Person p);
}
然后通过printPersons方法打印符合条件的成员信息:
public static void printPersons(List<Person> roster, CheckPerson tester) {
for (Person p : roster) {
if (tester.test(p)) {
p.printPerson();
}
}
}
最后因为CheckPerson 接口是一个函数式接口。(函数式接口是任何只包含一个抽象方法的接口。 一个函数式接口可能包含一个或多个默认方法或静态方法。)因为一个函数式接口只包含一个抽象方法,所以在实现它时可以省略该方法的名称。为此,我们可以使用 lambda 表达式:
public static void main(String[] args) {
List<Person> people = ......;
printPersons(people,(Person p)->{
return p.getGender() == Person.Sex.MALE&&
p.getAge() > 18 &&
p.getAge() < 28;
});
}
如果您觉得下面的代码很难理解,请看另一篇文章中的“Lambda 表达式的理想用例”小节,因为本文省略了方法一到方法二的演变过程。
方法二:使用Java自带的接口实现实现上述功能
在筛选年龄在18-25岁的男性成员后,打印他们的邮件地址:
修改printPersons方法:
public static <X,Y>void printPersons(List<X> roster, Predicate<X> tester, Function<X,Y> mapper, Consumer<Y> block) {
for (X x : roster) {
if (tester.test(x)) {
Y data = mapper.apply(x);
block.accept(data);
}
}
}
在调用过程中使用lambda表达式:
public static void main(String[] args) {
List<Person> people = ...;
printPersons(people,
p->{
return p.getGender() == Person.Sex.MALE&&
p.getAge() > 18 &&
p.getAge() < 28;
},
person -> {
return person.getEmailAddress();
},
email ->
System.out.println(email)
);
}
此方法调用执行以下操作:(为了便于理解可将X看作Person,Y看作String)
- 从集合 people中获取 Person 对象。
- 过滤与Predicate类型的tester匹配的对象。在此示例中,Predicate的对象是一个 lambda 表达式,用于指定哪些成员有资格使用选择性服务。
- 遍历过滤后的由Function类型的mapper指定的对象的值。在此示例中,Function的对象是一个返回成员电子邮件地址的 lambda 表达式。
- 对遍历的每个对象执行由Consumer类型的block方法指定的操作。在此示例中,Consumer 的对象是一个打印字符串的 lambda 表达式,该字符串是 Function 对象返回的电子邮件地址。
方法三:使用聚合操作改写上面的例子
以下示例使用聚合操作来打印集合中符合选择性服务资格的成员的电子邮件地址:
public static void main(String[] args) {
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方法操作 | 聚合操作 |
获取源对象 | 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) |
Lambda表达式的高级用法
访问封闭范围的局部变量
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 表达式的类型?
方法所期望的数据类型称为目标类型。为了确定 lambda 表达式的类型,Java编译器使用找到lambda表达式的上下文或情况的目标类型。
例如在调用方法一中的printPersons时,它需要 CheckPerson 数据类型,因此 lambda 表达式属于 CheckPerson 类型。
在Calculator 的示例中调用operateBinary时,他需要IntegerMath 数据类型,因此 lambda 表达式属于 IntegerMath 类型。
考虑下面的例子会输出什么?
public class TargetType {
static void invoke(Runnable r) {
System.out.println("Runnable");
r.run();
}
static <T> T invoke(Callable<T> c) throws Exception {
System.out.println("Callable");
return c.call();
}
public static void main(String[] args) throws Exception {
String s =invoke(() -> "done");
}
}
输出:Callable
方法 invoke(Callable) 将被调用,因为该方法返回一个值;方法 invoke(Runnable) 没有返回值。在这种情况下,lambda 表达式 () -> “done” 的类型是 Callable。
序列化
如果 lambda 表达式的目标类型及其捕获的参数是可序列化的,则可以序列化它。但是,与内部类一样,强烈建议不要对 lambda 表达式进行序列化。