Java lambda表达式与效果等同于final的变量

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 字符串是在构造函数内部定义的局部变量;nameresponse 变量是类的特性;ActionListener 接口以匿名内部类的形式实现,其中一个方法为 actionPerformed。请注意,内部类中的代码:

  • 可以访问特性(如 nameresponse

  • 可以修改特性(本例没有展示)

  • 可以访问局部变量(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?”。

赞(0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

Java 实例