概述
- 一个lambda表达式是一个带有参数的代码块;
- 当你想把代码块在以后的某个时间执行时,可以使用lambda表达式;
- lambda表达式可以转换为函数式接口;
- lambda表达式可以在闭包作用域中有效地访问final变量;
- 方法和构造器引用可以引用方法或构造器,但无需调用它们;
- 可以向接口添加默认(default)和静态(static)方法来提供具体的实现;
- 必须解决接口中多个默认方法之间的冲突。
lambda表达式的语法
Java中的lambda表达式的格式:参数、箭头->、以及一个表达式。
- 如果负责计算的代码无法用一个表达式表示,那么可以用编写方法的方式来编写:即用{}包裹代码并明确使用return语句,例如:
(String first, String second) -> {
if (first.length() < second.length())
return -1;
else if (first.length() > second.length())
return 1;
else
return 0;
}
- 如果lambda表达式没有参数,你仍然可以提供一对空的小括号,如同不含参数的方法一样:
() -> {
for (int i = 0; i < 10; i++) doSomething();
}
- 如果一个lambda的参数类型是可以被推导的,那就可以省略它们的类型,例如:
Comparator<String> comp = (first, second) -> Integer.compare(first.length(), second.length());//编译器会根据Lambda表达式对应的函数式接口Comparator<String>进行自动推断
- 如果某个方法只含有一个参数,并且该参数的类型可以被推导出来,甚至可以省略小括号:
EventHandler<ActionEvent> listener = event -> System.out.println("Hello world!");//无需(event) ->或者(ActionEvent event) ->
- 永远不要为一个lambda表达式执行返回类型,它总会从上下文中被推导出来。例如:
(first, second) -> Integer.compare(first.length(), second.length());//可以被使用在期望结果类型为int的上下文中。
注意:
- 可以向对待方法参数一样向lambda表达式的参数添加注解或final修饰符,如下:
(final String name) -> {}
(@NonNull String name) -> {}
- 在lambda表达式中,只在某些分支中返回值(其他分支没有返回值)是不合法的。例如:
(int x) -> {if (x > 0) return 1;}//是不合法的。
函数式接口
对于只包含一个抽象方法的接口,可以通过lambda表达式来创建该接口的对象。这种接口被称为函数式接口(Functional Interface)。也就是说:任何接口,如果只包含唯一一个抽象方法,那么它就是一个函数式接口。
例如:Runnable就是一个函数式接口。我们以Arrays.sort方法为例。在Java8之前,可以借助匿名内部类来实现:
List<String> words = Arrays.asList("apple", "banana", "pear");
words.sort(new Comparator<String>() {
@Override
public int compare(String w1, String w2) {
return Integer.compare(w1.length(), w2.length());
}
});
该方法的参数需要一个Comparator接口(该接口只有一个抽象方法,是一个函数式接口)的实例。接下来编写一个lambda表达式:
words.sort((String first, String second) -> {
return Integer.compare(first.length(), second.length());
});
在这个表达式背后,Arrays.sort会接收一个Comparator接口的类的实例。调用该对象的compare的方法会执行lambda表达式中的代码。
按照1.2节第3点,继续简化:
words.sort((first, second) -> {
return Integer.compare(first.length(), second.length());
});
如果lambda表达式的代码块只是return后面的一个表达式,还可以继续简化:
words.sort((first, second) -> Integer.compare(first.length(), second.length()));
任何可以接受一个函数式接口实例的地方,都可以用Lambda表达式。
方法引用
有些时候,你只想传递给其他代码的操作已经有实现的方法了。例如,你只想在按钮被点击时打印event对象,你可以像如下代码一样调用:
button.setOnAction(event -> System.out.println(event));
如果只调用println方法给setOnAction就更好了。下面是修改后的代码:
button.setOnAction(System.out::println);
表达式System.out::println是一个方法引用,类似于 x -> System.out.println(x)。
::操作符将方法名和对象或类的名字分隔开来。以下是三种主要的使用情况:
- 对象::实例方法
- 类::静态方法
- 类::实例方法
构造器引用
构造器引用与方法引用类似,不同的是构造器引用中方法名是new。例如Button::new表示Button类的构造器引用。
变量作用域
通常,我们希望能够在lambda表达式的闭合方法或类中访问其他的变量,例如:
public static void main(String[] args) {
repeatMessage("Hello", 20);
}
public static void repeatMessage(String text,int count){
Runnable r = () -> {
for(int i = 0; i < count; i++){
System.out.println(text);
Thread.yield();
}
};
new Thread(r).start();
}
注意看lambda表达式中的变量count和text,它们并没有在lambda表达式中被定义,而是方法repeatMessage的参数变量。如果你思考一下,就会发现这里有一些隐含的东西。lambda表达式可能会在repeatMessage返回之后才运行,此时参数变量已经消失了。如果保留text和count变量会怎样呢?
为了理解这一点,我们需要对lambda表达式有更深入的理解。一个lambda表达式包括三个部分:
- 一段代码
- 参数
- 自由变量的值//这里的“自由”指的是那些不是参数并且没有在代码中定义的变量。
在我们的示例中,lambda表达式有两个自由变量,text和count。数据结构表示lambda表达式必须存储这两个变量的值,即“Hello”和20。我们可以说,这些值已经被lambda表达式捕获了(这是一个技术实现的细节。例如,你可以将一个lambda表达式转换为一个只含一个方法的对象,这样自由变量的值就会被复制到该对象的实例变量中)。
如你所见,lambda表达式可以捕获闭合作用域中的变量值。在java中,为了确保被捕获的值是被良好定义的,需要遵守一个重要的约束。在lambda表达式中,被引用的变量的值不可以被更改。例如,下面这个表达式是不合法的:
public static void repeatMessage(String text,int count){
Runnable r = () -> {
while(count > 0){
count--; //错误,不能更改已捕获变量的值
System.out.println(text);
Thread.yield();
}
};
new Thread(r).start();
}
做出这个约束是有原因的。更改lambda表达式中的变量不是线程安全的。假设有一系列并发的任务,每个线程都会更新一个共享的计数器。
lambda表达式的方法体与嵌套代码块有着相同的作用域。因此它也适用同样的命名冲突和屏蔽规则。在lambda表达式中不允许声明一个与局部变量同名的参数或者局部变量。
Path first = Paths.get("/usr/bin");
Comparator<String> comp = (first,second) ->
Integer.compare(first.length(),second.length());
//错误,变量first已经定义了
在一个方法里,你不能有两个同名的局部变量,因此,你也不能在lambda表达式中引入这样的变量。