Java lambda表达式与效果等同于final的变量,用户希望从 lambda 表达式内部访问在其外部定义的变量,必须将在 lambda 表达式内部访问的局部变量声明为 final
,或使其具备等同于 final
的效果(effectively final)。可以对特性(attribute)进行访问和修改。
Java lambda表达式与效果等同于final的变量 问题描述
用户希望从 lambda 表达式内部访问在其外部定义的变量。
Java lambda表达式与效果等同于final的变量 解决方案
必须将在 lambda 表达式内部访问的局部变量声明为 final
,或使其具备等同于 final
的效果(effectively final)。可以对特性(attribute)进行访问和修改。
Java lambda表达式与效果等同于final的变量 具体实例
20 世纪 90 年代末,在 Java 初登舞台时,开发人员偶尔会使用 GUI 库 Swing1 来编写客户端 Java 应用程序。与所有 GUI 库一样,Swing 组件也是事件驱动的。换言之,组件产生事件,监听器(listener)对事件做出响应。
1一种基于 Java 的跨平台 MVC 框架,采用单线程模型,属于 JFC 的一部分。——译者注
为每个组件创建单独的监听器被视为一种良好实践,因此监听器通常作为匿名内部类实现。使用内部类不仅有助于保持程序的模块化,内部类中的代码还可以访问并修改外部类的私有特性。例如,JButton
实例生成 ActionEvent
,而 ActionListener
接口包含一个名为 actionPerformed
的单一抽象方法。一旦实现被注册为监听器,就会调用该方法。相关示例如例 5-4 所示。
例 5-4 简单的 Swing GUI
public class MyGUI extends JFrame {
private JTextField name = new JTextField("Please enter your name");
private JTextField response = new JTextField("Greeting");
private JButton button = new JButton("Say Hi");
public MyGUI() {
// 无关的GUI设置代码
String greeting = "Hello, %s!"; ➊
button.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
response.setText(
String.format(greeting, name.getText()); ➋
// greeting = "Anything else"; ➌
}
});
}
}
❶ 局部变量
❷ 访问局部变量和特性
❸ 修改局部变量(无法编译)
在本例中,greeting
字符串是在构造函数内部定义的局部变量;name
和 response
变量是类的特性;ActionListener
接口以匿名内部类的形式实现,其中一个方法为 actionPerformed
。请注意,内部类中的代码:
- 可以访问特性(如
name
和response
) -
可以修改特性(本例没有展示)
-
可以访问局部变量(
greeting
) -
无法修改局部变量
在 Java 8 之前,编译器要求 greeting
变量被声明为 final
。而在 Java 8 中,变量不必采用 final
修饰,但必须具备等同于 final
的效果。换言之,任何试图修改局部变量值的代码都不会被编译。
当然,在 Java 8 中,应采用 lambda 表达式替换匿名内部类,如例 5-5 所示。
例 5-5 监听器的 lambda 表达式
String greeting = "Hello, %s!";
button.addActionListener(e ->
response.setText(String.format(greeting,name.getText())));
同样地,greeting
变量不必被声明为 final
,但必须具备等同于 final
的效果,否则代码无法编译。
如果读者对 Swing 示例不感兴趣,我们再来讨论另外一个示例。如例 5-6 所示,我们希望对给定 List
中的所有值求和。
例 5-6 对
List
中的所有值求和
List<Integer> nums = Arrays.asList(3, 1, 4, 1, 5, 9);
int total = 0; ➊
for (int n : nums) { ➋
total += n;
}
total = 0;
nums.forEach(n -> total += n); ➌
total = nums.stream() ➍
.mapToInt(Integer::valueOf)
.sum()
❶ 局部变量 total
❷ 传统的 for-each 循环
❸ 修改 lambda 表达式中的局部变量(无法编译)
❹ 将流转换为 IntStream
并调用 sum
方法
上述代码声明了一个名为 total
的局部变量,并采用传统的 for-each 循环对所有值求和。
Iterable
接口定义的 forEach
方法传入 Consumer
作为参数。如果 Consumer
试图修改 total
变量,则代码不会编译。
当然,解决这个问题的正确方式是将流转换为 IntStream
。由于它定义了 sum
方法,不会涉及任何局部变量。
严格来说,函数以及在其环境中定义的可访问变量称为闭包(closure)。从这一定义来看,Java 对局部变量的处理并不是很明确:虽然可以访问局部变量,但无法修改。在 Java 8 中,lambda 表达式是通过值(而非变量)来关闭的,读者或许认为 lambda 表达式实际上属于闭包 2。
2那么,为什么不将 Java 8 引入的 lambda 表达式称为闭包呢?根据 Bruce Eckel 的说法,原因在于“闭包”这个术语的应用过于频繁,因而引发了争议。“当人们讨论真正的闭包时,往往意味着他们在讨论第一种语言遇到的闭包”。感兴趣的读者可以参考 Bruce 的博文“Are Java 8 Lambdas Closures?”。