这篇Java教程基于JDK1.8。教程中的示例和实践不会使用未来发行版中的优化建议。
Lambda表达式
对于简单的匿名类来说,比如一个简单的接口只有唯一的方法,那么匿名类的语法看起来显得笨拙而不清楚。这时你可以把一个函数当做参数传递给方法,比如当有人点击按钮时应该触发什么动作。Lambda表达式就是这样,可以让你把函数当做参数,把代码当做数据。
在匿名类一节中,向你介绍了如何在不提供名字的基础上实现一个类。这通常比命名类更简洁,但是对于只有一个方法的类,即使是匿名类也显得有些多余和麻烦。Lambda表达式让你可以更简洁地表示单方法类的实例。
这一节包含下面这些主题:
Lambda表达式的理想用例
假设你创建了一个社交应用。你想增加一个特性,该特性能让管理员完成任何动作:比如在满足特定条件的社交网络成员中发送一条消息。下表描述了这个场景中的详细情况:
字段 | 描述 |
---|---|
Name | 在指定的成员上完成操作 |
Primary Actor | 管理员 |
Preconditions | 管理员已经登陆进入系统 |
Postconditions | 动作只对符合指定条件的成员上执行完成 |
Main Success Scenario | 1.管理员指定符合条件的成员 2.管理员指定要完成的动作 3.管理员触发提交动作 4.系统查找到所有符合条件的成员 5.系统在符合条件的成员上完成指定动作 |
Extensions | 管理员可以选择在触发指定动作之前预览符合特定条件的成员 |
Frequency of Occurrence | 一天之中会发生多次 |
假设社交应用中的成员用如下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表达式来完成。
方法1:创建搜索匹配一个特征的成员的方法
最简单的就是创建多个方法,每个方法匹配一个特征,比如性别或者年龄。下面这个方法打印出比给定年龄大的成员。
public static void printPersonsOlderThan(List<Person> roster,int age){
for(Person p : roster){
if(p.getAge() > age){
p.printPerson();
}
}
}
方法2:创建更通用的搜索方法
下面的方法相比printPersonsOlderThan更通用,它打印出指定年龄范围内的成员:
public static void printPersonsWithinAgeRange(List<Person> roster,int low,int high){
for(Person p : roster){
if(p.getAge() >= low && p.getAge() <= high){
p.printPerson();
}
}
}
方法3:在局部类中指定搜索条件代码
下面的方法打印出符合指定条件的成员:
public static void printPersons(List<Person> roster,CheckPerson tester){
for(Person p : roster){
if(tester.test(p)){
p.printPerson();
}
}
}
为实现特定的条件,你可以实现CheckPerson接口:
interface CheckPerson{
boolean test(Person p);
}
如下的类实现了CheckPerson接口:
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());
方法4:在匿名类中指定搜索条件代码
printPersons(roster,
new CheckPerson(){
public boolean test(Person p){
return p.gender == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25;
}
});
方法5:使用Lambda表达式指定搜索条件代码
printPersons(roster,
(Person p) - > p.gender == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25;
);
方法6:使用带有Lambda表达式的标准函数接口
考虑下CheckPerson接口:
interface CheckPerson{
public boolean test(Person p);
}
因为只包含唯一的方法所以它是一个函数接口。该方法接收一个参数返回一个boolean值。该方法太简单了所以不值得我们在应用中来定义它,因为JDK已经定义了一些函数接口,你可以在java.util.function包中找到。
比如,你可以使用Predicate<T>接口来代替CheckPerson。该接口包含一个方法boolean test(T t)。
interface Predicate<T>{
boolean test(T t);
}
该接口是泛型接口的一个示例。参数化的Predicate<Person>如下所示:
interface Predicate<Person>{
boolean test(Person t);
}
此时,你可以使用泛型化后的Predicate<Person>来代替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表达式的其他地方。
方法7:在整个应用程序中使用Lambda表达式
看看下面的代码中还有哪里可以使用Lambda表达式:
public static void printPersonsWithPredicate(List<Person> roster,Predicate<Person> tester){
for(Person p : roster){
if(tester.test(p)){
p.printPerson();
}
}
}
该方法检查roster集合中的每个Person实例是否满足Predicate实例tester中定义的条件。如果Person实例确实满足tester实例定义的条件。Person实例上的printPersron 方法将会被调用。
除了调用printPerson方法外,你可以在Person实例上当满足tester所指定的条件时指定一个动作。这个动作可以用Lambda表达式来表示。如果你需要一个与printPerson类似的方法,有一个参数并且无返回值。记住,当你要使用Lambda表达式时,记得要实现一个函数接口。在本例中,你需要一个函数接口包含一个抽象方法,该抽象方法需要一个Person类型的参数并且无返回值。Consumer<T>接口有一个void accept(T t)方法,符合这个特征。下面的示例使用Consumer<Person>实例的accept方法代替了p.printPerson()。
public static void processPersons(List<Person> roster,
Precedicate<Person> tester,
Consumer<Person> block){
for(Person p : roster){
if(tester.test(p)){
block.accept(p);
}
}
}
那么调用方法如下:
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);
}
}
}
下面的代码从每个Person实例中获得了email address并把它打印出来:
processPersonsWithFunction(roster,
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25,
p -> p.getEmailAddress(),
email -> System.out.println(email)
);
方法8:更广泛地使用泛型
看看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);
}
}
}
那么打印符合条件Person的email address的方法调用方式如下:
processElements(roster,
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25,
p -> p.getEmailAddress(),
p -> System.out.println(p));
这个方法调用完成了下面几个动作:
- 获取source集合中的元素
- 过滤匹配tester的对象
- 使用mapper映射每一个过滤后对象为一个值
- 用block对每一个映射对象完成一个动作
你可以使用聚合来代替这每一个操作。
方法9:使用接受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));
GUI应用中的Lambda表达式
在JavaFX 示例中,你可以用Lambda表达式替换代码中的高亮部分:
btn.setOnAction(new EventHandler(){
@Override
public void handler(ActionEvent event){
System.out.println(“Hello World!”);
}
});
由于这个接口是函数式接口,你可以用Lambda表达式像下面这样来表示:
btn.setOnAction(event -> System.out.println("Hello World!"));
Lambda表达式语法
Lambda表达式由下面这几部分组成:
- 用逗号分隔的形式参数列表,由括号括起来。CheckPerson.test方法有一个参数p,表示Person类的实例。
注意:在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表达式,你必须用{}来包裹语句。但对无返回值的方法调用可以不用{}包裹。下面是一个合法的表达式:
email -> System.out.println(email);
注意:Lambda表达式看起来很像方法声明;你可以认为lambda表达式是一个匿名方法。
下面的示例,演示了有多个形式参数的Lambda表达式的用法:
public class Caculator{
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){
Caculator myApp = new Caculator();
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,substraction));
}
}
该示例输出如下:
40 + 2 = 42
20 - 10 = 10
访问封闭范围内的局部变量
public class LambdaScopeTest {
public int x = 0;
class FirstLevel{
public int x = 1;
void methodInFirstLevel(int x){
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 = (x) -> {
// ...
}
编译器将会报错"variable x is already defined in method methodInFirstLevel(int)",因为lambda表达式不会产生新一级的范围。因此,你能直接访问封闭范围内的变量、方法和局部变量。比如:在methodInFirstLevel方法中lambda表达式可以直接访问参数x。为访问封闭类的变量可以使用this关键字。在本例中this.x其实是指FirstLevel.x。
但是,如同局部和匿名类,lambda表达式只能访问封闭块中用 final修饰的或者实际上是final的局部变量。比如当你在方法声明methodInFirstLevel后紧接着以下这行代码:
void methodInFirstLevel(int x) {
x = 99;
// ...
}
因为这个赋值语句,导致FirstLevel.x 不再是final类型了。结果,在后面访问FirstLevel.x的时候编译器将会报错。
System.out.println("x = " + x);
目标类型
怎么决定lambda表达式的类型呢?
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25
该lambda表达式被用在下面两种场景:
- public static void printPersons(List<Person> roster, CheckPerson tester)
- public void printPersonsWithPredicate(List<Person> roster, Predicate<Person> tester)
当Java运行时调用printPersons方法时,它期望类型是CheckPerson,那么lambda表达式就是这个类型。但是当调用printPersonsWithPredicate方法时,它期望类型是Predicate<Person> tester,那么lambda表达式就是这个类型。这些方法期望的类型我们称作目标类型。为确定lambda表达式的类型,Java编译器使用找到lambda表达式的上下文的目标类型。因此,只能在Java编译器可以确定目标类型的情况下使用lambda表达式:
- 变量声明
- 赋值
- 返回语句
- 数组初始化
- 方法和构造参数
- lambda表达式体
- 条件表达式 ?:
- 转换表达式
目标类型与方法参数
对方法参数而言,Java编译器决定目标类型是依赖于两个语言特性:重载解析和类型参数推断。
考虑下面两个接口:
public interface Runnable{
void run();
}
public interface Callable<V>{
V call();
}
方法run没有返回值,方法call有返回值。
假设你重载了一个invoke方法:
void invoke(Runnable r){
r.run();
}
<T> T invoke(Callable<T> v){
return v.call();
}
下面的语句会调用哪个方法:
String s = invoke(() -> "done");
方法invoke(Callable<T>)将会被调用因为该方法会返回一个值。方法invoke(Runnable)没有返回值。在本例中,lambda表达式 () -> “done” 类型是 invoke(Callable<T>)。
序列化
如果目标类型及其捕获的参数是可序列化的,则可以序列化lambda表达式。但是就像局部类一样,强烈建议不要对lambda表达式进行序列化。