lambda表达式是Java8带给我们的几个重量级特性之一,借用lambda表达式,可以让我们的Java程序设计更加简洁和高效。要深入理解lambda表达式,需要理解函数式接口,lambda表达式的表示形式,以及方法引用。
1. 函数式接口
函数式接口是只有一个抽象方法的接口,用作lambda表达式的类型。
函数式接口可以用@FunctionalInterface注解,可以把它放在注解的前面,但它是非必须的,使用注解只是为了方便编译器作语法检查。只要接口只包含一个抽象方法,虚拟机会自动判断。在接口中添加了 @FunctionalInterface 的注解后,该接口就只允许有一个抽象方法,否则编译器也会报错。如下,java.lang.Runnable 就是一个函数式接口:
1 |
|
函数式接口的重要属性是:我们能够使用 lambda 实例化它们,lambda 表达式让你能够将函数作为方法参数,或者将代码作为数据对待。
2. lambda表达式
lambda表达式是一种紧凑的传递行为的方式。lambda表达式由三部分组成:第一部分为一个括号内用逗号分隔的形式参数,参数是函数式接口里面方法的参数;第二部分为一个箭头符号:->;第三部分为方法体,可以是表达式和代码块。语法如下:
方法体为表达式,该表达式的值作为返回值返回
1
(parameters) -> expression
例如:
1
Supplier<String> i = ()-> "Supplier test";
这里,”Supplier test”就是lambda表达式i的get方法返回值。
方法体为代码块,必须用 {} 来包裹起来,且需要一个 return 返回值,但若函数式接口里面方法返回值是 void,则无需返回值。
1
(parameters) -> { statements; }
例如上面的例子的等价形式为:
1
Supplier<String> i = ()-> {return "Supplier test";};
下面是匿名内部类的代码:
1
2
3
4
5
6button.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
System.out.print("Hello lambda in actionPerformed");
}
});下面是使用lambda表达式后的代码:
1
2
3
4button.addActionListener(
//actionPerformed 有一个参数 e 传入,所以用 (ActionEvent e)
(ActionEvent e)-> System.out.print("Hello lambda in actionPerformed")
);其实,lambda表达式可以自己根据上下文推断参数类型,无需显示指定:
1
button.addActionListener( e -> System.out.print("Hello lambda in actionPerformed"));
lambda表达式的几种变体
将lambda表达式赋值给一个一个变量,lambda表达式没有参数:1
Runnable noArguments = () -> System.out.println("Hello World"); //(1)
将lambda表达式赋值给一个一个变量,lambda表达式的参数类型由编译器推导出来:
1
ActionListener oneArgument = event -> System.out.println("button clicked"); //(2)
将lambda表达式赋值给一个一个变量,lambda表达式有多个参数类型,参数类型由编译器推导出来:
1
BinaryOperator<Long> add = (x, y) -> x + y; //(3)
将lambda表达式赋值给一个一个变量,lambda表达式有多个参数类型,显示声明lambda表达式类型:
1
BinaryOperator<Long> addExplicit = (Long x, Long y) -> x + y; //(4)
如上所示,有lambda表达式中的参数类型都是由编译器推断得出的。这当然不错,但有时最好也可以显式声明参数类型,此时就需要使用小括号将参数括起来,多个参数的情况也是如此,如(4)。
注意:
目标类型是指lambda表达式所在上下文环境的类型。比如,将lambda表达式赋值给一个局部变量,或传递给一个方法作为参数,局部变量或方法参数的类型就是lambda表达式的目标类型。lambda表达式的目标类型类型依赖于上下文环境,是由编译器推断出来的。如(1)和(2)的lambda表达式的目标类型分别是Runnable和ActionListener类型,可以把lambda表达式看作实现了该接口的内部类的实例,noArguments和oneArgument分表表示指向实例的引用。
lambda表达式能引用表达式之外定义的既成事实的final变量。虽然无需将变量声明为final,但在lambda表达式中,也无法用作非终态变量。下面的可以编译通过:
1
2String name = getUserName();
button.addActionListener(event -> System.out.println("hi" + name));但下面不能编译通过:
1
2
3String name = getUserName();
name = formatUserName(name);
button.addActionListener(event -> System.out.println("hi" + name));也就是说,lambda只能引用表达式之外不会改变的变量,之所以有这样的限制,是因为若变量可以改变,并发执行多个lambda表达式时就会不安全。
3. 方法引用
可以用已经定义好的类中的方法来表示一个lambda表达式。例如:1
button.addActionListener(event -> System.out.println(event));
可以使用System.out::println的方法引用表示成如下形式:1
button.addActionListener(System.out::println);
总的来说,方法引用有如下四种使用情况:
- 对象::实例方法
- 类::静态方法
- 类::实例方法
- 类::new
在前两种情况,方法引用等同于提供方法参数的lambda表达式。如System.out::println等同于x -> System.out.println(x)。类似的,Math::pow等同于(x, y) -> Math.pow(x, y)。
第三种情况,第一个参数为执行方法的对象,即当lambda表达式的的第一个参数是要执行的方法体所属的对象时,可以使用类::实例方法的方法引用代替。例如:String::compareToIgnoreCase等同于(x, y) -> x.compareToIgnoreCase(y),这里x就是compareToIgnoreCase方法所属对象。其实这也是合理的,因为Java中实例方法拥有者为一个具体的对象,只有一个对象才能调用实例方法。
下面是一个具体的引用类::实例方法的例子,其中personList.stream().forEach(x -> x.getName())和personList.stream().forEach(Person::getName)是等价的。
1 | import java.util.ArrayList; |
第四种情况则是构造器的引用,对于拥有多个构造器的类,选择使用哪个引用取决于上下文。需要注意的是,虽然方法引用使用的是一个方法,但不需要在后面加括号,因为这里并不调用该方法。使用该方法只是提供了一种和lambda表达式等价的一种结构,在需要时才会调用。凡是使用lambda表达式的地方,就可以使用方法引用。
方法引用还可以使用this或super参数,表示引用本类或父类中的方法,例如:this::equals就等同于x -> this.equals(x)。
1 | public class ConcurrentGreeter extends Greeter { |
该例子中,相当于引用父类的方法,实现了一个这样的lambda表达式,返回类型为Runnable:1
Thread t = new Thread(() -> System.out.println("Hello, world"));
4. 什么时候使用Lambda表达式
函数式接口是lambda表达式的类型,因此,若函数的形参传递的是一个函数式接口类型的引用,则可以直接给该形参传递lambda表达式。当然,也可以给该形参传递实现该接口的匿名类对象或实例。下面的三种方法是等效的,当然,使用lambda表达式是最简洁的。
1 | import java.util.Arrays; |
参考: