(1)为什么引入lambda表达式
lambda
表达式是一个可传递的代码块,可以在以后执行一次或多次。
指定时间间隔完成工作。可以将这个工作放到一个ActionListener
的actionPerformed
方法中:
class Worker implements ActionListener {
public void actionPerformed(Action event) {
// do some work
}
}
想要反复执行这个代码时,可以构造一个Worker
类的实例。然后把这个实例提交到一个Timer
对象。这里的重点是actionPerformed
方法包含希望以后执行的代码。
或者可以考虑如何用一个定制比较器完成排序。如果想按长度而不是默认的字典顺序对字符串排序,可以向sort
方法传入一个Comparator
对象:
class LengthComparator implements Comparator<String> {
public int compare(String first, String second) {
return first.length() - second.length();
}
}
...
Arrays.sort(String, new LengthComparator());
compare
方法不是立即调用。实际上,在数组完成排序之前,sort
方法会一直调用compare
方法,只要元素的顺序不正确就会重新排列元素。将比较元素所需的代码段放在sort
方法中,这个代码将与其余的排序逻辑集成。
这两个例子有一个共同点,都是将一个代码块传递到某个对象(一个定时器,或者一个sort
方法)。这个代码块会在将来的某个时间调用。
到目前为止,在Java
中传递一个代码段并不容易,不能直接传递代码段。Java
是一种面向对象语言,所以必须构造一个对象,这个对象的类需要有一个方法能包含所需的代码。
(2)lambda表达式的语法
lambda
表达式就是一个代码款,以及必须传入代码的变量规范。
参数,箭头(->)以及一个表达式。
(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 = 100; i >= 0 ; i --) System.out.println(i)};
如果可以推导出lambda表达式的参数类型,则可以忽略其类型:
Comparator<String> comp
= (first, second) //Same as(String first, String second)
-> first.length() - second.length();
在这里,编译器可以推导出first和second必然是字符串,因为这个lambda
表达式将赋给一个字符串比较器。
如果方法只有一个参数,而且这个参数的类型可以推导出,那么甚至可以省略小括号:
ActionListener listener
= event -> System.out.priintln("This time is " + new Date());
//Instead of (event) -> ... or (ActionEvent event) -> ...
无需指定lambda表达式的返回类型。lambda
表达式的返回类型总是会由上下文推导得出:
(String first, String second) -> first.length() - second.length();
可以在需要int类型结果的上下文中使用。
(3)函数式接口
前面已经讨论过,Java中已经有很多封装代码块的接口,如ActionListener
或Comparator
。lambda
表达式与这些接口是兼容的。
对于只有一个抽象方法的接口,需要这种接口的对象时,就可以提供一个lambda表达式。这种接口称为函数式接口。
为了展示如何转换为函数式接口,下面考虑Array.sort
方法。它的第二个参数需要一个Comparator
实例,Comparator就是只有一个方法的接口,所以可以提供一个lambda表达式:
Arrays.sort(words, (first, second) -> first.length() - second.length());
在底层,Arrays.sort
方法会接收实现了Comparator<String>
的某个类的对象。在这个对象上调用compare
方法会执行这个lambda表达式的体。这些对象和类的管理完全取决于具体实现,与使用传统的内联相比,这样可能高效得多。最好把lambda表达式看作是一个函数,而不是一个对象,另外要接受lambda表达式可以传递到函数式接口。
lambda表达式可以转换为接口,这一点让lambda表达式很有吸引力。
Timer t = new Timer(1000, event -> System.out.println("At the tone, the time is " + new Date()));
Toolkit.getDefaultToolkit().beep();
与实现了ActionListener
接口的类相比,这个代码可读性要好得多。
实际上,在Java
中,对lambda表达式所能做的也只是能转换为函数式接口。
Java
API在java.util.function包中定义了很多非常通用的函数式接口。其中一个接口==BiFunction<T,U,R>==描述了参数类型为T和U而且返回类型为R的函数。可以把我们的字符串比较lambda表达式保存在这个类型的变量中:
BiFunction<String, String, Integer> comp =
(first, second) -> first.length() - second.length();
(4)构造器引用
构造器引用与方法引用很类似,只不过方法名为new
。例如,Person::new
是Person
构造器的一个引用。哪一个构造器呢?这取决于上下文。假设你有一个字符串列表。可以把它转换为一个Person
对象数组,为此要在各个字符串上调用构造器,调用如下:
ArrayList<String> names = ...;
Stream<Person> stream = name.stream().map(Person::new);
List<Person> people = stream.collect(Collectors.toList());
(5)变量作用域
通常,你可能希望在lambda表达式中访问外围方法或类中的变量。考虑下面这个例子:
public static void repeatMessage(String text, int delay) {
ActionListener listener = event -> {
System.out.println(text);
Toolkit.getDefaultToolkit.beep();
};
new Timer(delay, listener).start();
}
//看这样一个调用:
respeatMessage("hello", 1000);//prints hello every 1000 milliseconds
现在来看lambda表达式中的变量text。注意这个变量并不是在lambda表达式中定义的。实际上,这是repeatMessage方法的一个参数变量。
如果再想想看,这里好像会有问题,尽管不那么明显。lambda表达式的代码可能会在repeatMessage调用返回很久以后才运行,而那时这个参数变量已经不存在了。如何保留text变量呢?
lambda表达式有3个部分:
1)一个代码块
2)参数
3)自由变量的值,这里只非参数而且不在代码中定义的变量。
我们的例子中,这个lambda表达式有1个自由变量text。表示lambda表达式的数据结构必须存储自由变量的值,在这里就是字符串“hello”,我们说它被lambda表达式捕获。(下面来看具体的实现细节。例如,可以把一个lambda表达式转换为包含一个方法的对象,这样自由变量的值就会复制到这个对象的实例变量中。)
注释:关于代码块以及自由变量的值有一个术语:闭包(closure)。在Java中,lambda表达式就是闭包。
可以看到,lambda表达式可以捕获外围作用域中变量的值。,在Java
中,要确保所捕获的值是明确定义的,这里有一个重要的限制。==在lambda表达式中,只能引用值不会改变的变量。例如:
public static void countDown(int start, int delay) {
ActionListener listener = event ->
{
start --;
System.out.println(start);
}
new Timer(delay, listener).start();
}
之所以有这个限制是有原因的。如果在lambda表达式中改变变量,并发执行多个动作时就会不安全。对于目前为止我们看到的动作不会发生这种情况,不过一般来讲,这确实是一个严重的问题。
另外如果在lambda表达式中引用变量,而这个变量可能在外部改变,这也是不合法的。:
public static void repeat(String text, int count)
{
for(int i =1; i <= count; i ++) {
ActionListener listener = event ->
{
System.out.println(i + ":" + text);
};
new Timer(1000, listener).start();
}
}
这里有一条规则:lambda表达式中捕获的变量必须实际上是最终变量。实际上的最终变量是指,这个变量初始化之后就不会再为它赋新值。在这里,text总是指示同一个String对象,所以捕获这个变量是合法的。不过,i的值会改变,因此不能捕获i。
lambda表达式的体与嵌套块有相同的作用域。这里同样适用命名冲突和遮蔽的有关规则。在lambda表达式中声明与一个局部变量同名的参数或局部变量是不合法的。
(6)处理lambda表达式
==使用lambda表达式的重点是延迟执行。==之所以希望以后再执行代码,这有很多原因,如:
*在一个单线程中运行代码;
*多次运行代码;
*在算法的适当位置运行代码(例如,排序中的比较操作);
*发生某种情况时执行代码(如,点击了一个按钮,数据到达,等等);
*只在必要时才运行代码。