1.为什么引入lambda表达式
lambda表达式是一个可传递的代码块,可以在以后执行一次或多次。具体介绍语法之前,下面先退一步,观察我们在Java中的那些地方用过这种代码块。
如何用一个定制比较器完成排序。如果想按长度而不是默认的字典顺序队字符串进行排序,可以向sort方法传入一个Comparator对象:
class LengthComparator implements Comparator<String>
{
public int compare(String first,String second)
{
return first.length()-second.length();
}
}
...
Arrays.sort(strings,new LengthComparator());
compare方法不是立即调用。实际上,在数组完成排序之前,sort方法会一直调用compare方法,只要元素的顺序不正确就会重新排列元素。将比较元素所需的代码段放在sort方法中,这个代码将与其余的排序逻辑集成。这个例子有个特点,就是将一个代码块传递到某个对象,这个代码块会在将来某个时间调用。
到目前为止,在Java中传递一个代码段并不容易,你不能直接传递代码段。Java是一种面向对象语言,所以必须构造一个对象,这个对象的类需要有一个方法包含所需的代码。
2.lambda表达式的语法
再考虑上面讨论的排序例子。我们传入代码来检查一个字符串是否比另一个字符串短。这里要计算:
first.length()-second.length()
first和second是什么它们都是字符串。Java是一种强类型语言,所以我们还要指定它们的类型:
(String first,String second)->first.length()-second.length();
这就是你看到的第一个lambda表达式。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=100;i>=0;i--)System.out.println(i);}
如果方法只有一个参数,而且这个参数的类型可以推导得出,那么甚至还可以省略小括号:
ActionListener listener=event->System.out.println("...");
注:如果一个lambda表达式只在某些分值返回一个值,而另外一些分支不返回值,这是不合法。
下面程序显示了如何对一个比较器使用lambda表达式。
3.函数式接口
对于只有一个抽象方法的接口,需要这种接口的对象时,就可以提供一个lambda表达式。这种接口称为函数式接口。
为了展示如何转换为函数式接口,下面考虑Arrays.sort方法。它的第二个参数需要一个Comparator实例,Comparator就是只有一个方法的接口,所以可以提供了一个lambda表达式:
Arrays.sort(words,(first,second)->first.length()-second.length());
在底层,Arrays.sort方法会接收实现了Comparator的某个类的对象。在这个对象上调用compare方法会执行这个lambda表达式的体。这些对象和类的管理完全取决于具体实现,与使用传统的内联类相比,这样可能要高效得多。最好把lambda表达式看作是一个函数而不是一个对象,另外要接受lambda表达式可以传递到函数式接口。
4.变量作用域
通常,你可能希望能够在lambda表达式中访问外围方法或类中的变量。考虑下面这个例子:
public static void repeatMessage(String text,int delay)
{
ActionListener listener=event->
{
System.out.println(text);
Toolkit.getDefaultToolkit().beep();
};
new Timer(delay,listener).start();
}
来看这样一个调用:
repeatMessage("Hello",1000);//prints Hello every 1,000 milliseconds
现在来看lambda表达式中的变量text。注意这个变量并不是在这个lambda表达式中定义的。实际上,这是repeatMessage方法的一个参数变量。
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 next,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.
5.处理lambda表达式
使用lambda表达式的重点是延迟执行。毕竟,如果想要立即执行代码,完全可以直接执行,而无须把它包装在一个lambda表达式中。之所以希望以后再执行代码,这有很多原因,如:
- 在一个单独的线程中运行代码
- 多次运行代码
- 在算法的适当位置运行代码(例如,排序中的比较操作)
- 发生某种情况时执行代码(如,点击了一个按钮,数据到达,等等)
- 值在必要时才运行代码。