一、为什么要引入lambda表达式
lambda 表达式是一个可传递的代码块
,
可以在以后执行一次或多次
。
在介绍lambda表达式之前,我们看一下,以前,我们对于一个问题的通常写法。
假设你已经了解了如何按指定时间间隔完成工作,当然不了解也没关系,只是作为例子说明。
将这个工作放在一个 ActionListener 的
actionPerformed
方法中
:
class Worker implements ActionListener
{
public void actionPerformed(ActionEvent 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();
}
}
这两个例子有一些共同点,
都是将一个代码块传递到某个对象
(
一个定时器
,
或者一个
sort
方法)。这个代码块会在将来某个时间调用。
到目前为止,
在
Java
中传递一个代码段并不容易
,
不能直接传递代码段 。
Java
是一种面
向对象语言
,
所以必须构造一个对象
,
这个对象的类需要有一个方法能包含所需的代码。
在其他语言中,
可以直接处理代码块
。
Java
设计者很长时间以来一直拒绝增加这个特性
。 毕竟,
Java
的强大之处就在于其简单性和一致性
。
如果只要一个特性能够让代码稍简洁一些
, 就把这个特性增加到语言中,
这个语言很快就会变得一团糟
,
无法管理
。 不过
,
在另外那些 语言中,
并不只是创建线程或注册按钮点击事件处理器更容易
;
它们的大部分
API
都更简单
、 更一致而且更强大。
在
Java
中
,
也可以编写类似的
API
利用类对象实现特定的功能
,
不过这种 API
使用可能很不方便
。
二、lambda表达式的语法
再来考虑上面讨论的排序例子。
我们传入代码来检查一个字符串是否比另一个字符串短。
这里要计算
:
first.
length()
-
second
.
length()
first 和
second
是什么
?
它们都是字符串
。
Java
是一种强类型语言
,
所以我们还要指定它 们的类型:
(String first, String second)
-> first.length() - second.length()
这就是一个lambda表达式。lambda
表达式就是一个代码块
,
以及必须传入代码的变量规范。
你已经见过 Java
中的一种
lambda
表达式形式
:
参数
,
箭头
(
-
>
)
以及一个表达式
。
如果代码要完成的计算无法放在一个表达式中,
就可以像写方法一样
,
把这些代码放在
{}
中
, 并包含显式的 return
语句
。
例如
:
即使 lambda 表达式没有参数
,
仍然要提供空括号
,
就像无参数方法一样
:
如果可以推导出一个 lambda
表达式的参数类型
,
则可以忽略其类型
。
例如
:
在这里,
编译器可以推导出
first
和
second
必然是字符串
,
因为这个
lambda
表达式将赋给一个字符串比较器。
如果方法只有一个参数
,
而且这个参数的类型可以推导得出
,
那么甚至还可以省略小括号
:
无需指定 lambda
表达式的返回类型
。
lambda
表达式的返回类型总是会由上下文推导得出。
例如
,
下面的表达式:
可以在需要 int
类型结果的上下文中使用
。
如果一个 lambda
表达式只在某些分支返回一个值
,
而在另外一些分支不返回值
, 这是不合法的。
例如
,
(
int
x
)
-
>
{
if
(
x
>
=
0
)
return
1
;
}
就不合法
。
代码示例:
package FunctionProm;
import javax.swing.*;
import java.util.Arrays;
import java.util.Date;
public class LambdaTest {
public static void main(String[] args) {
String[] planets = new String[] { "Mercury" , "Venus" , "Earth" , "Mars" , "Jupiter" , "Saturn" , "Uranus" , "Neptune" };
System.out.println(Arrays.toString(planets));
System.out. println("Sorted in dictionary order:") ;
Arrays.sort(planets);
System.out.println (Arrays.toString(planets));
System.out . println ("Sorted by length:");
Arrays.sort(planets, (first, second) -> first.length() - second.length()) ;
System.out. println(Arrays.toString(planets));
Timer t = new Timer(1000, event ->
System.out.println ("The time is " + new Date()));
t.start();
// keep program running until user selects "0k"
JOptionPane.showMessageDialog (null , "Quit program?");
System.exit(0);
}
}
三、函数式接口
前 面 已 经 讨 论 过,
Java
中 已 经 有 很 多 封 装 代 码 块 的 接 口
,
如
ActionListener
或 Comparator。
lambda 表达式与这些接口是兼容的。
对于只有一个抽象方法的接口,
需要这种接口的对象时
,
就可以提供一个
lambda
表达式。
这种接口称为函数式接口
(
functional
interface
)
。
为了展示如何转换为函数式接口,
下面考虑
Arrays
.
sort
方法
。
它的第二个参数需要一个 Comparator 实例
,
Comparator
就是只有一个方法的接口
,
所以可以提供一个
lambda
表达式
:
在底层,
Arrays
.
sort
方法会接收实现了
Comparator
<
String
>
的某个类的对象
。 在这个对象上调用 compare 方法会执行这个 lambda 表达式的体。这些对象和类的管理完全取决于具体实现, 与使用传统的内联类相比,这样可能要高效得多。最好把 lambda 表达式看作是一个函数,而不是一个对象, 另外要接受 lambda 表达式可以传递到函数式接口。
lambda 表达式可以转换为接口
,这一点让
lambda
表达式很有吸引力
。
具体的语法很简短。
下面再来看一个例子
:
与使用实现了 ActionListener
接口的类相比
,
这个代码可读性要好得多
。
实际上,
在 Java
中,
对
lambda
表达式所能做的也只是能转换为函数式接口
。
在其他支 持函数字面量的程序设计语言中,
可以声明函数类型
(
如
(
String
,
String
)
-
>
int
)
、
声明这些类 型的变量,
还可以使用变量保存函数表达式
。
不过
,
Java
设计者还是决定保持我们熟悉的接口概念,
没有为Java
语言增加函数类型
。
Java API
在
java
.
util
.
fimction
包中定义了很多非常通用的函数式接口
。
其中一个接口BiFunction<
T
,
U
,
R
> 描述了参数类型为
T
和
U
而且返回类型为
R
的函数
。
可以把我们的字符串比较 lambda
表达式保存在这个类型的变量中
:
不过,
这对于排序并没有帮助
。
没有哪个
Arrays
.
sort
方法想要接收一个
BiFunction
。
如果你之前用过某种函数式程序设计语言,
可能会发现这很奇怪
。
不过
,
对于
Java
程序员而言,
这非常自然
。
类似
Comparator
的接口往往有一个特定的用途
,
而不只是提供一个有指定参数和返回类型的方法。
Java
SE
8
沿袭了这种思路
。
想要用
lambda
表达式做某些处理
,
还是要谨记表达式的用途,
为它建立一个特定的函数式接口
。
java
.
util
.
function
包中有一个尤其有用的接口
Predicate
:
ArrayList 类有一个
removelf
方法
,
它的参数就是一个
Predicate
。
这个接口专门用来传递 lambda 表达式
。
例如
,
下面的语句将从一个数组列表删除所有
null
值
:
list.
removelf
(
e
-
>
e
=
=
null
)
;
四、方法引用
有时,
可能已经有现成的方法可以完成你想要传递到其他代码的某个动作
。
例如
,
假设你希望只要出现一个定时器事件就打印这个事件对象。
当然
,
为此也可以调用
:
但是,
如果直接把
println
方法传递到
Timer
构造器就更好了
。
具体做法如下
:
表达式 System
.
out
::
println
是一个方法引用
(
method reference
)
,
它等价于
lambda
表达式 x 一
>
System
.
out
.
println
(
x
) 。
再来看一个例子,
假设你想对字符串排序
,
而不考虑字母的大小写
。
可以传递以下方法表达式:
从这些例子可以看出,
要用::
操作符分隔方法名与对象或类名
。
主要有
3
种情况
:
在前 2
种情况中
,
方法引用等价于提供方法参数的
lambda
表达式
。
前面已经提到
, System.
out
::
println 等价于
x
-
>
System
.
out
.
println
(
x)。 类似地,
Math
:
:
pow
等价于
(
x
,
y
)
-
>
Math
.
pow
(
x
,
y)。
对于第 3
种情况
,
第 1
个参数会成为方法的目标
。
例如
,
String
:
:
compareToIgnoreCase
等
同于
(
x
,
y
)
-
>
x
.
compareToIgnoreCase
(
y
) 。
如果有多个同名的重栽方法,
编译器就会尝试从上下文中找出你指的那一个方法
。 例如,
Math
.
max
方法有两个版本
,
一个用于整数
,
另一个用于
double
值
。
选择哪一个版 本取决于 Math
::
max
转换为哪个函数式接口的方法参数
。
类似于
lambda
表达式
,
方法引用不能独立存在,
总是会转换为
函数式接口
的实例
。
可以在方法引用中使用 this 参数
。
例如
,
this
::
equals
等同于
x
-
>
this
.
equals
(
x
)
。
使用
super
也是合法的
。
下面的方法表达式:
super:
:
instanceMethod
使用 this
作为目标
,
会调用给定方法的超类版本
为了展示这一点
,
下面给出一个假想的例子
:
TimedGreeter.
greet
方法开始执行时
,
会构造一个
Timer
,
它会在每次定时器滴答时执行 super::
greet
方法
。
这个方法会调用超类的
greet
方法
。
五、构造器引用
构造器引用与方法引用很类似,
只不过方法名为
new
。
例如
,
Person
:
:
new
是
Person
构造 器的一个引用。
哪一个构造器呢
?
这取决于上下文
。
假设你有一个字符串列表
。
可以把它转换为一个 Person
对象数组
,
为此要在各个字符串上调用构造器
,
调用如下
:
map 方法会为各个列表元素调用
Person
(
String
)
构造器
。
如果有多个
Person
构造器
,
编译器会选择有一个 String
参数的构造器
,
因为它从上下文推导出这是在对一个字符串调用构造器。
可以用数组类型建立构造器引用。
例如
,
int
[]
::
new
是一个构造器引用
,
它有一个参数
:即数组的长度。
这等价于
lambda
表达式
x
-
>
new int
[
x
]。
Java 有一个限制
,
无法构造泛型类型 T
的数组
。
数组构造器引用对于克服这个限制很有用。
表达式
new
T
[
n
]
会产生错误
,
因为这会改为
new
Object
[
n]。
对于开发类库的人来说,
这是一个问题。
例如
,
假设我们需要一个
Person
对象数组。
Stream 接口有一个
toArray
方法可以返回 Object
数组
:
不过,
这并不让人满意
。
用户希望得到一个
Person
引用数组
,
而不是
Object
引用数组
。流库利用构造器引用解决了这个问题。
可以把
Person
[
]
:
:
new 传入
toArray
方法
:
toArray方法调用这个构造器来得到一个正确类型的数组
。
然后填充这个数组并返回
。
六、变量作用域
通常,
你可能希望能够在
lambda
表达式中访问外围方法或类中的变量
。
考虑下面这个例子:
public static void repeatMessage(String text, int delay)
{
ActionListener listener = event ->
{
System.out.println(text);
Toolkit.getDefaultToolkit().beep():
};
new Timer(delay, listener).start();
}
来看这样一个调用:
现在来看 lambda
表达式中的变量
text
。
注意这个变量并不是在这个
lambda
表达式中定义的。
实际上
,
这是
repeatMessage
方法的一个参数变量
。
如果再想想看,
这里好像会有问题
,
尽管不那么明显
。
lambda
表达式的代码可能会在 repeatMessage 调用返回很久以后才运行
,
而那时这个参数变量已经不存在了
。
如何保留
text
变量呢
?
要了解到底会发生什么,
下面来巩固我们对
lambda
表达式的理解
lambda
表达式有
3 个部分:
1 )
一个代码块
;
2 )
参数
;
3 )
自由变量的值
,
这是指非参数而且不在代码中定义的变量
。
在我们的例子中,
这个 lambda
表达式有
1
个自由变量
text
。
表示
lambda
表达式的数据结构必须存储自由变量的值,
在这里就是字符串
"
Hello
"
。
我们说它被
lambda 表达式捕获(下面来看具体的实现细节
。
例如
,
可以把一个
lambda
表达式转换为包含一个方法的对象,
这样自由变量的值就会复制到这个对象的实例变量中
。
)