Java8 之 Lambda 表达式
Lambda 表达式并不是什么新概念,Java8 中引入它主要解决只有单一方法的匿名类使用起来过于丑陋、晦涩的问题。不说废话。假设有这样一个对象:
public class Person {
public enum Sex {
MALE, FEMALE
}
String name;
LocalDate birthday;
Sex gender;
String emailAddress;
public int getAge() {
// ...
}
public void printPerson() {
// ...
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
如果想从一组 Person
中打印特定年龄的,代码看起来是这样的:
public static void printPersonsOlderThan(List<Person> roster, int age) {
for (Person p : roster) {
if (p.getAge() >= age) {
p.printPerson();
}
}
}
2
3
4
5
6
7
如果查找年龄的算法变了,你可能会把代码改成这样:
public static void printPersonsWithinAgeRange(
List<Person> roster, int low, int high) {
for (Person p : roster) {
if (low <= p.getAge() && p.getAge() < high) {
p.printPerson();
}
}
}
2
3
4
5
6
7
8
这时你意识到,算法的改变总是会导致修改上面的代码,为了应对将来可能的变化,你想到了把查找 Person
的算法从中剥离出来:
public static void printPersons(
List<Person> roster, CheckPerson tester) {
for (Person p : roster) {
if (tester.test(p)) {
p.printPerson();
}
}
}
2
3
4
5
6
7
8
这时只需要针对不同的查找算法提供不同的实现类:
interface CheckPerson {
boolean test(Person p);
}
class CheckPersonEligibleForSelectiveService implements CheckPerson {
public boolean test(Person p) {
return p.gender == Person.Sex.MALE &&
p.getAge() >= 18 &&
p.getAge() <= 25;
}
}
2
3
4
5
6
7
8
9
10
11
调用的时候,只需要实例化特定的实现类并传入方法即可:
printPersons(roster, new CheckPersonEligibleForSelectiveService());
然而这样的方式会引入一堆“很小”的实现类。于是你发现这种场景下使用匿名类更为合适:
printPersons(
roster,
new CheckPerson() {
public boolean test(Person p) {
return p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25;
}
}
);
2
3
4
5
6
7
8
9
10
1. Lambda 表达式的使用
CheckPerson
是一个函数接口,它是指只有一个抽象方法的接口(友情提示: 没有任何方法的接口叫标识接口)。由于只有一个抽象方法,我们完全有理由在实现中忽略掉方法名,而这正是 Lambda 表达式的作用:
printPersons(
roster,
(Person p) -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25
);
2
3
4
5
6
再回顾一下 CheckPerson
接口的代码:
interface CheckPerson {
boolean test(Person p);
}
2
3
这样的函数接口过于简单,似乎没有必要在应用中定义诸多类似的东西。于是 JDK 给你提供了很多开箱即用的接口,它们在 java.util.function
包中。比如该例中可以使用 Predicate<T>
:
interface Predicate<T> {
boolean test(T t);
}
2
3
使用它来替换 CheckPerson
接口,代码变成了:
public static void printPersonsWithPredicate(
List<Person> roster, Predicate<Person> tester) {
for (Person p : roster) {
if (tester.test(p)) {
p.printPerson();
}
}
}
2
3
4
5
6
7
8
对应的调用方式为:
printPersonsWithPredicate(
roster,
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25
);
2
3
4
5
6
玩完了年龄的查找算法,我们来看一下执行逻辑。前面的示例在找到合法的年龄后,只是调用了 printPerson()
方法进行打印。如果想改变这一执行逻辑,不再是打印呢?我们可以将执行过程当作一个参数并使用 lambda 表达式来改造。要记住,使用 lambda 表达式必须实现函数接口。
Consumer<T>
接口可以传入一个参数并返回 void
,该接口包含一个方法:void accept(T t)
,使用它将代码改为:
public static void processPersons(
List<Person> roster,
Predicate<Person> tester,
Consumer<Person> block) {
for (Person p : roster) {
if (tester.test(p)) {
block.accept(p);
}
}
}
2
3
4
5
6
7
8
9
10
这时便可以将不同的执行逻辑当作参数传入,如果依然想执行打印方法,则:
processPersons(
roster,
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25,
p -> p.printPerson()
);
2
3
4
5
6
7
有时会需要处理后得到一个返回值,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);
}
}
}
2
3
4
5
6
7
8
9
10
11
12
调用代码,获得用户的电子邮件地址并输出到控制台:
processPersonsWithFunction(
roster,
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25,
p -> p.getEmailAddress(),
email -> System.out.println(email)
);
2
3
4
5
6
7
8
2. 充分使用泛型
事实上,我们上面得到的 processPersonsWithFunction
方法不仅可以处理 Person
类型,如果我们把类型声明改用泛型,可以得到更为通用的方法:
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);
}
}
}
2
3
4
5
6
7
8
9
10
11
12
针对 Person
类型,依然可以如此调用:
processElements(
roster,
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25,
p -> p.getEmailAddress(),
email -> System.out.println(email)
);
2
3
4
5
6
7
8
它的执行过程是这样的:
- 从集合 source 中获取一组对象,该例从 roster 中获得 Person 对象。
- 从这组对象中过滤出符合 tester 的( 即满足 Predicate 的对象 )。该例的 Predicate 使用 lambda 表达式。
- 通过 Function 类型的对象 mapper ,将上面过滤出的对象类型映射为某值类型。该例中的 Function 对象是 Lambda 表达式,它返回了 String 型的电子邮件地址。
- 将映射后的值通过 Consumer 对象 block 执行某行为。该例中的 Consumer 对象也是 Lambda 表达式,执行的行为是打印字符串。
3. 使用链式操作
使用 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));
2
3
4
5
6
7
8
例子中用到的方法签名如下:
- Stream stream()
- Stream filter(Predicate<? super T> predicate)
- Stream map(Function<? super T,? extends R> mapper)
- void forEach(Consumer<? super T> action)
参考此例,可以在 Java 中写出许多能够媲美支持函数式编程的语言的代码。
4. 在 GUI 中的应用
Java GUI 编程中经常出现类似的代码:
btn.setOnAction(new EventHandler<ActionEvent>() {
@Override
public void handle(ActionEvent event) {
System.out.println("Hello World!");
}
});
2
3
4
5
6
它极其适合使用 Lambda 表达式进行改造:
btn.setOnAction(
event -> System.out.println("Hello World!")
);
2
3
5. 语法细节
领略了 Lambda 表达式的风采后,来看其语法。Lambda 表达式由三部分组成:
- 形参列表
- 箭头符号
- 主体部分
首先是放在圆括号中的、以逗号分隔的形参列表。比如:
(User u, Role r) -> ...
在 Lambda 表达式中可以省略参数类型,当只有一个参数时,也可以省略圆括号:
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25
2
3
箭头符号就是 ->
。
主体可以是一个表达式:
p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25
2
3
也可以是一个语句:
p -> {
return p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25;
}
2
3
4
5
无返回值的语句也可以不使用语句块({}
):
email -> System.out.println(email)
来看一个使用多个形参的例子:
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));
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
它的输出结果为:
40 + 2 = 42
20 - 10 = 10
2
6. 作用域
Lambda 表达式中可以使用变量,采用词法作用域(ps:可参考 JavaScript 作用域),不存在变量作用域屏蔽问题。
import java.util.function.Consumer;
public class LambdaScopeTest {
public int x = 0;
class FirstLevel {
public int x = 1;
void methodInFirstLevel(int x) {
// 该语句将会导致 `语句A` 编译错误:
// "local variables referenced from a lambda expression
// must be final or effectively final"
// x = 99;
Consumer<Integer> myConsumer = (y) ->
{
System.out.println("x = " + x); // 语句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);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
上例的输出结果为:
x = 23
y = 23
this.x = 1
LambdaScopeTest.this.x = 0
2
3
4
如果把例子中的 myConsumer
的参数改为 y
:
Consumer<Integer> myConsumer = (x) -> {
// ...
}
2
3
将会抛出错误: "variable x is already defined in method methodInFirstLevel(int)" 。这是因为 Lambda 表达式并不会引入新的作用域范围。Lambda 表达式中只能访问 final 或 effectively final 的变量,这一点和匿名类仍然是一致的。
effectively final 是 Java8 的特性之一:局部内部类和匿名内部类访问的局部变量必须由final修饰,Java8 中可以不加final修饰符,由系统默认添加。
7. 目标类型
Lambda 表达式的类型是由上下文推导而来,并不是函数接口的名称。例如之前的两个例子:
- public static void printPersons(List roster, CheckPerson tester)
- public void printPersonsWithPredicate(List roster, Predicate tester)
当 Java 的运行时执行 printPersons
方法时需要 CheckPerson
类型,因此 Lambda 表达式的类型是 CheckPerson
。而在方法 printPersonsWithPredicate
中,其类型为 Predicate<Person>
。这种类型叫做 目标类型(target type) ,它只能在下列场景使用:
- 变量声明
- 赋值
- 返回语句 return
- 数组初始化
- 方法或构造器参数
- Lambda 表达式主体中
- 条件表达式 ? :
- 类型转换
最后来看一个例子。假设有如下两个函数接口:
public interface Runnable {
void run();
}
public interface Callable<V> {
V call();
}
2
3
4
5
6
7
Runnable.run
没有返回值,而 Callable<V>.call
有返回值。
现在有这样两个方法:
void invoke(Runnable r) {
r.run();
}
<T> T invoke(Callable<T> c) {
return c.call();
}
2
3
4
5
6
7
下面的语句会执行哪个方法呢?
String s = invoke(() -> "done");
答案是 invoke(Callable<T>)
,因为它有返回值。因此,Lambda 表达式的类型是 Callable<T>
,很简单对吗?
参考资料
@ssbunny 2015-10-30