这篇Java教程基于JDK1.8。教程中的示例和实践不会使用未来发行版中的优化建议。

Lambda表达式

对于简单的匿名类来说,比如一个简单的接口只有唯一的方法,那么匿名类的语法看起来显得笨拙而不清楚。这时你可以把一个函数当做参数传递给方法,比如当有人点击按钮时应该触发什么动作。Lambda表达式就是这样,可以让你把函数当做参数,把代码当做数据。

在匿名类一节中,向你介绍了如何在不提供名字的基础上实现一个类。这通常比命名类更简洁,但是对于只有一个方法的类,即使是匿名类也显得有些多余和麻烦。Lambda表达式让你可以更简洁地表示单方法类的实例。

这一节包含下面这些主题:

Lambda表达式的理想用例

假设你创建了一个社交应用。你想增加一个特性,该特性能让管理员完成任何动作:比如在满足特定条件的社交网络成员中发送一条消息。下表描述了这个场景中的详细情况:

字段描述
Name在指定的成员上完成操作
Primary Actor管理员
Preconditions管理员已经登陆进入系统
Postconditions动作只对符合指定条件的成员上执行完成
Main Success Scenario1.管理员指定符合条件的成员
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表达式进行序列化。