内容来自《Java 8函数式编程》
1. Lambda表达式
1.1 第一个Lambda表达式
Swing 是一个与平台无关的 Java 类库,用来编写图形用户界面(GUI)。该类库有一个常见用法:为了响应用户操作,需要注册一个事件监听器。用户一输入,监听器就会执行一些操作
1 | button.addActionListener(new ActionListener() { |
在这个例子中,我们创建了一个新对象,它实现了 ActionListener 接口。这个接口只有一个方法 actionPerformed
。匿名内部类实现了该方法。
但是匿名内部类不够简便,样板代码,可读性很差,我们不想传入对象,只想传入行为。
Java 8 中,上述代码可以写成一个 Lambda 表达式
1 | button.addActionListener(event -> System.out.println("button clicked")); |
event 是参数名,和上面匿名内部类示例中的是同一个参数。-> 将参数和 Lambda 表达式的主体分开,而主体是用户点击按钮时会运行的一些代码
Lambda 表达式中无需指定类型,程序依然可以编译。这是因为 javac 根据程序的上下文(
addActionListener
方法的签名)在后台推断出了参数 event 的类型。这意味着如果参数类型不言而明,则无需显式指定。
为了增加可读性并迁就我们的习惯,声明参数时也可以包括类型信息,而且有时编译器不一定能根据上下文推断出参数的类型!
1.2 如何辨别Lambda表达式
几种 Lambda 表达式的变体
1 | // 1 |
- 使用空括号 () 表示没有参数,实现了 Runnable 接口,该接口也只有一个 run 方法,返回类型为 void
- 只包含一个参数,可省略参数的括号
- 主体可以是一段代码块,可以用返回或抛出异常来退出,只有一行代码的 Lambda 表达式也可使用大括号
- Lambda 表达式也可以表示包含多个参数的方法,这行代码创建了一个函数,用来计算两个数字相加的结果。变量 add 的类型是
BinaryOperator
,它不是两个数字的和,而是将两个数字相加的那行代码 - 可以显式声明参数类型,需要使用小括号将参数括起来
目标类型是指 Lambda 表达式所在上下文环境的类型。比如,将 Lambda 表达式赋值给一个局部变量,或传递给一个方法作为参数,局部变量或方法参数的类型就是 Lambda 表达式的目标类型。
1.3 引用值,而不是变量
在使用匿名内部类时,当你需要引用它所在方法里的变量,需要将变量声明为 final
1 | final String name = getUserName(); |
Java 8 虽然放松了这一限制,可以引用非 final 变量,但是该变量在既成事实上必须是 final。虽然无需将变量声明为 final,但在 Lambda 表达式中,也无法用作非终态变量。如果坚持用作非终态变量,编译器就会报错。
- 既成事实上的 final 是指只能给该变量赋值一次。
- 换句话说,Lambda 表达式引用的是值,而不是变量。
1 | String name = getUserName(); |
如果你试图给该变量多次赋值,然后在 Lambda 表达式中引用它,编译器就会报错:
local variables referenced from a Lambda expression must be final or effectively final
1
2
3 String name = getUserName();
name = formatUserName(name);
button.addActionListener(event -> System.out.println("hi " + name));
1.4 函数接口
函数接口是只有一个抽象方法的接口,用作 Lambda 表达式的类型
例如 Swing 中的 ActionListener
, 只有一个抽象方法:actionPerformed
,被用来表示行为。该接口也继承自一个不具有任何方法的父接口:EventListener
。
1 | public interface ActionListener extends EventListener { |
接口中单一方法的命名并不重要,只要方法签名和 Lambda 表达式的类型匹配即可。
- 这里的函数接口接受一个
ActionEvent
类型的参数,返回空(void)
JDK 提供了一组核心函数接口会频繁出现,以下罗列一部分
接口 | 参数 | 返回类型 | 示例 |
---|---|---|---|
Predicate<T> |
T | boolean | 这张唱片已经发行了吗 |
Consumer<T> |
T | void | 输出一个值 |
Function<T, R> |
T | R | 获得 Artist 对象的名字 |
Supplier<T> |
None | T | 工厂方法 |
UnaryOperator<T> |
T | T | 逻辑非(!) |
BinaryOperator<T> |
(T, T) | T | 求两个数的乘积(*) |
1.5 类型推断
Lambda 表达式中的类型推断,实际上是 Java 7 就引入的目标类型推断的扩展,例如 Java 7 中的菱形操作符
1 | Map<String, Integer> diamondWordCounts = new HashMap<String, Integer>(); |
如果将构造函数直接传递给一个方法,也可根据方法签名来推断类型
1 | // 需要java8 |
Java 8 更进一步,可省略 Lambda 表达式中的所有参数类型, javac 根据 Lambda 表达式上下文信息就能推断出参数的正确类型。
程序依然要经过类型检查来保证运行的安全性,但不用再显式声明类型罢了。这就是所谓的类型推断。
一些示例
使用 Lambda 表达式检测一个 Integer 是否大于 5。这实际上是一个 Predicate (用来判断真假的函数接口)
1 | Predicate<Integer> atLeast5 = x -> x > 5; |
- Predicate 只有一个泛型类型的参数,Integer 用于其中。
- Lambda 表达式实现了 Predicate 接口,因此它的单一参数被推断为 Integer 类型。
- javac 还可检查 Lambda 表达式的返回值是不是 boolean,这正是 Predicate 方法的返回类型
Predicate 接口的源码,接受一个对象,返回一个布尔值
1
2
3 public interface Predicate<T> {
boolean test(T t);
}
BinaryOperator:该接口接受两个参数,返回一个值,参数和值的类型均相同。实例中所用的类型是 Long
1 | BinaryOperator<Long> addLongs = (x, y) -> x + y; |
但若信息不够,类型推断系统也无能为力,例如代码修改为
BinaryOperator add = (x, y) -> x + y;
编译器给出的报错信息如下:
Operator '& #x002B;' cannot be applied to java.lang.Object, java.lang.Object.
上面的例子中并没有给出变量 add 的任何泛型信息,给出的正是原始类型的定义。因此,编译器认为参数和返回值都是 java.lang.Object 实例。
1.6 要点回顾
- Lambda 表达式是一个匿名方法,将行为像数据一样进行传递。
- Lambda 表达式的常见结构:
BinaryOperator add = (x, y) → x + y
- 函数接口指仅具有单个抽象方法的接口,用来表示 Lambda 表达式的类型。