有没有想过,如果匿名类非常简单,比方说一个接口仅有一个方法,实现这个接口的匿名类表达方式就显得很别扭。 而且,往往在这种情况下,你是把一个匿名类作为参数传给另外一个方法使用的。 因为匿名类经常是实现接口完成功能的, 也就是说你要把一个功能作为参数传给另外一个方法,感觉怪怪的,不是吗 (通常情况我们是传数据,不是传行为)? 就好比,点击一个按钮要完成特定的功能这种情况。 Lambda表达式可以简化这些,它可以把功能当作参数, 把代码调用当成数据在不同的地方传输。
在前一节,我们说过匿名类, 就是说如何实现一个基类而不用指出子类名字。相对每个子类都要起个名字,这已经是比较简洁的做法了,然后对于只有一个方法的父类来说,这样仍然显得有点多余。Lambda表达式就是用来优美的实例化只有一个方法的类。
让我们来看一个Labda表达式使用的理想案例。
Labda表达式使用的理想案例
假定你正在创建一个社交网站, 需要实现这样的功能:管理员能执行任何操作,其中一种操作就是能给符合条件的成员发送消息。下面的表格详细描述了该功能:
字段 | 描述 |
---|---|
功能名字 | 对选定的成员执行相应的操作 |
主要执行角色 | 管理员 |
前置条件 | 管理员已经登陆进系统 |
后置条件 | 执行的操作只会应用到符合条件的成员上 |
主要的步骤 | 1.管理员指定要选出成员的条件 2.管理员对选定的成员发作操作指令 3.管理员选中提交按钮 4.系统后台查找所有符合条件的成员 5.系统对所有符合条件的会员执行相应操作 |
扩展场景 | 管理员在执行提交操作之前, 能够预览他/她选中的符合条件的成员 |
发生频率 | 每天很多次 |
我们假定社交网站里的成员是用下面的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
实现方式1:创建方法来查找符合特定条件的成员
最最简单的方式就是创建它几个方法,每个方法查找符合特定条件的成员,例如根据性别或者年龄。下面的方法就可以输出年龄大于一定岁数的会员:
public static void printPersonsOlderThan(List<Person> roster, int age) {
for (Person p : roster) {
if (p.getAge() >= age) {
p.printPerson();
}
}
}
温馨提示:一个列表(List)就是一个有序的集合(Collection). 集合就是把多个元素归并到一个单位, 它就是用来存储、获取、操纵以及传输聚合性数据的。如果想要了解更多关于集合的信息,点击Collections
这种实现方式会使你的系统很脆弱, 也就是说一旦有小的更新或者改动 (例如引入新的数据类型),整个系统可能就不工作了。设想一下,如果你更新你的系统,把Person
的结构更改一下,增加新的成员变量或者计算年龄的算法变了。 那你就不得不重写你的很多方法来满足此次更新。而且这个方案限制性太强,设想一下如果你想要输出年龄小于一定岁数的会员,怎么办?
实现方式2:创建更通用的查找方法
下面的方法比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
方法强了不少,但是它只适用于年龄这个属性上。 如果其它属性还需要创建新的方法。 对每种查询条件都创建一个方法还是会使系统显得不那么强壮。你可以把代码分离, 查询条件单独写到一个类中去。
实现方式3:用局部类实现查询条件
下面的方法就会输出满足你你指定条件的成员:
public static void printPersons(
List<Person> roster, CheckPerson tester) {
for (Person p : roster) {
if (tester.test(p)) {
p.printPerson();
}
}
}
这个方法会检查列表参数roster
中的每一个Person
实例,看它是不是符合参数tester(CheckPerson类型)
所指定的条件。 通过tester.test
来调用检查,如果返回true
,就是满足,然后person
的printPerson
方法就会被调用 。
为了定义查询条件,你需要实现CheckPerson
接口:
interface CheckPerson {
boolean test(Person p);
}
下面的类实现了接口CheckPerson
, 实现了接口的test方法。 这个方法会查出在美国适合服兵役的成员。方法会返回true, 如果成员是男性并且在18到25岁之间。
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
实现了一个接口,可以考虑用匿名类来代替局部类, 这样就不用为每个查询都声明一个新类了。
实现方式4:用匿名类指定查询条件
下面的方法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表达式来代替匿名类,我们会在下一节中介绍。
实现方式5:用Lambda表达式指定查询条件
CheckPerson
接口是一个函数式接口。任何只包含一个抽象方法(一个函数式接口可能用一个或多个默认方法和静态方法)的接口都可以叫做函数式接口。因为函数式接口只包含了唯一一个抽象方法,当你在实现它的时候,你可以省略掉方法名字。 要到达到这样效果, 你可以用Lambda表达式来代替匿名类:
printPersons(
roster,
(Person p) -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25
);
要想了解更多关于如何定义Lambda表达式,可以查看Lambda表达式语法。
你还可以用标准的函数式接口来代替CheckPerson
,这样能减少更多的代码。
实现方式6: 接合标准的函数式接口和Lambda表达式
再看一下CheckPerson
接口
interface CheckPerson {
boolean test(Person p);
}
这是一个非常简单的接口。它是函数式接口因为它只有一个抽象方法。这个方法有一个参数一个布尔类型的返回值。这个方法是如此的简单,也许根本不用在你的程序中重写。因为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里面的调用效果是一样,都是找出在美国适合符兵役的成员。
printPersonsWithPredicate(
roster,
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25
);
这也不是唯一可以用Lambda表达式的地方,我们可以在其它很多地方使用Lambda表达式。
实现方案7: 让Lambda表达式遍布你的应用
让我们再来看一看这个方法printPersonsWithPredicate
有没有其它地方可以使用Lambda表达式
public static void printPersonsWithPredicate(
List<Person> roster, Predicate<Person> tester) {
for (Person p : roster) {
if (tester.test(p)) {
p.printPerson();
}
}
}
方法会遍历roster
列表中的每个Person
,看一下它是否满足tester
里所指定的条件。如果条件满足, Person
里面的printPerson
方法就会被调用。
如果不调用方法printPerson
,你可以给Person
定义另外一种操作, 当满足tester
指定的条件,就执行此操作。可以用Lambda表达式来定义这个新操作。 假定你想用Lambda表达式实现printPerson
, 只有一个参数Person
,并且返回空。别忘了,要用Lambda表达式,你需要实现一个函数接口。 因此我们先要定义一个函数接口, 它包含唯一的抽象方法,参数是Person
,并且返回空。 Consumer<T>
接口包含了方法 void accept(T t)
刚好满足这种情况, 真TM凑巧呀。接下来,我们就用Consumer<T>
的一个实例调用方法void accept(T t)
来替换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
有一样的功效,输出适合服兵役的人。
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);
}
}
}
下面的方法调用就会获取适合服兵役的会员邮件地址并且输出出来 。
processPersonsWithFunction(
roster,
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25,
p -> p.getEmailAddress(),
email -> System.out.println(email)
);
实现方案8: 更惨无人道的使用泛型
让我们再来看看下面的方法, 这是一个泛型的版本,它能接收一个数据集合为参数, 这个数据集合可以包含任何类型的数据元素
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)
);
我们来分析一下这个方法调用过程:
- 从集合(Collection)
source
中获取一个调用时传进来的源对象。在这个例子中,方法会从传进来roster
获取到一个Person
对象。注意roster是一个集合类型的列表,也是Iterable的对象类型。 - 然后开始过滤对象, 要符合
Predicate
对象tester
所指定的条件。在我们这个例子中,我们传的Predicate
对象是一个Lambda表达式,它指定了需要服兵役的条件。 - 根据函数式对象
Mapper
, 把过滤的对象或者对象属性映射到另外一个具体的值或对象上。 在我们这个例子中,函数式对象是一个Lambda表达式,会返回一个成员的电子邮件地址。 - 对每个映射的对象做相应的操作, 这个操作是
Consumer
的对象block
来定义的. 在我们的例子中,Consumer
对象是一个Lambda表达式,它会输出一个字串,也就是函数式对象返回的电子邮件地址。
上面的每一个操作你都可以用聚合操作来代替。
实现方式9: 使用聚合操作,把Lambda当作参数
下面的例子使用了聚合操作, 它会把集合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
方法的原因)。一个流就是一个元素序列。不像集合,它不是一个存储元素的数据结构。 相反的,流通过管道存储了具体的值,例如来源于collection的值。一个管道有一系列的流操作,在这个例子中就是filter- map-forEach
。 而且聚合操作接受Lambda表达式作为参数,能够使你自定义操作行为。
如果想要了解更多的聚合操作, 参照这里聚合操作
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表达式的语法
一个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在运行时会估算表达式并把结果返回,因此你也可以这样写:
p -> { return p.getGender() == Person.Sex.MALE && p.getAge() >= 18 && p.getAge() <= 25; }
请注意,一个返回语句不是表达式,你必须用{}包起来,然而如果一个方法返回的是空,你到是不需要用{},例如,下面的是正确的:
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表达式没有任何变量遮蔽的问题(想了解更多关于遮蔽, 看这里)。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表达式括号定义的y
换成x
, 编译就会报错。
错误会提示:“变量x已经在方法methodInFirstLevel(int)里定义了”,因为Lambda表达式不会引入新一级的变量作用域。相应的,你可以直接访问包装这个Lambda表达式代码块内的字段、方法以及局部变量。例如Lambda表达式就可以直接访问方法methodInFirstLevel
的参数x
。 如果要访问包装它类内的变量,就用this
。在这个例子中,this.x
指的就是类成员变量FirstLevel.x
。
然而,就如同局部类和匿名类一样,Lambda表达式只能访问final或者事实上是final的局部变量或者参数。例如,如果你在方法methodInFirstLevel
定义之后加上一句赋值语句:
void methodInFirstLevel(int x) {
x = 99;
// ...
}
因为这个赋值语句,变量FirstLevel.x
不再是事实上的final, 那么编译就会报错:Lambda表达式引用的局部变量必须是final或者事实上的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)
出现在 实现方式3:用局部类实现查询条件public void printPersonsWithPredicate(List<Person> roster, Predicate<Person> tester)
出现在实现方式6: 接合标准的函数式接口和Lambda表达式
当Java运行器调用方法printPersons
时,它期望的数据类型是CheckPerson
, Lambda表达式就会是这种类型。然而当Java运行器调用printPersonsWithPredicate
时, 它期望的类型是Predicate<Person>
, 而Lambda表达式就变成了这种类型。这些方法所期望的类型,我们叫做目标类型。为了判定一个Lambda表达式的类型,java编译器会在能找到Lambda表达式的上下文或者情形下使用的目标类型来决定。也就说,Lambda表达式只能使用在java编译器可以判定出目标类型的地方:
- 变量声明时
- 赋值时
- 返回语句(Return)
- 数组初始化时
- 方法或者构造器的参数
- Lambda表达式的内容里
- 条件表达式 ?:
- 类型转换表达式 (Cast)
目标类型和方法参数
关于方法参数,Java编译器在判定目标类型的时候用到了语言的另外两个特性:重载解析和类型参数推断。
我们来看一下下面的两个函数式接口:( java.lang.Runnable 和java.util.concurrent.Callable)
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();
}
那么下面的语句,哪一个invoke方法会被调用到?
String s = invoke(() -> "done");
invoke(Callable<T>)
方法会被调用到,因为这个方法是有返回值的方法。invoke(Runnable)
方法不会被调用到。在这种情况下,Lambda表达式 () -> "done"
的数据类型就是Callable<T>
序列化
如果一个Lambda表达式的目标类型和携带参数都是可序列化的,你可以序列化这个Lambda表达式。然而,就像匿名类,强烈建议别瞎捣腾去序列化Lambda表达式。