Java 中的闭包 - 捕获的值 - 为什么会出现这种意外结果?

2022-09-01 17:59:03

我想我在JavaScript中遇到了这种经典情况。

通常程序员会期望下面的代码打印“Peter”,“Paul”,“Mary”。

但事实并非如此。谁能确切地解释为什么它在Java中以这种方式工作?

此 Java 8 代码编译正常,并打印 3 次“Mary”。

我想这是一个如何在内心深处实现的问题,
但是......这是否表明底层实现是错误的?

import java.util.List;
import java.util.ArrayList;

public class Test008 {

    public static void main(String[] args) {
        String[] names = { "Peter", "Paul", "Mary" };
        List<Runnable> runners = new ArrayList<>();

        int[] ind = {0};
        for (int i = 0; i < names.length; i++){ 
            ind[0] = i;
            runners.add(() -> System.out.println(names[ind[0]]));
        }

        for (int k=0; k<runners.size(); k++){
            runners.get(k).run();
        }

    }

}

相反,如果我使用增强的for循环(在添加Runnables时),则会捕获正确的(即所有不同的)值。

for (String name : names){
    runners.add(() -> System.out.println(name));
}

最后,如果我使用经典的for循环(在添加Runnables时),那么我会收到一个编译错误(这是非常有意义的,因为变量不是最终的或有效的最终的)。i

for (int i = 0; i < names.length; i++){ 
    runners.add(() -> System.out.println(names[i]));
}

编辑:

我的观点是:为什么不捕获的值(在我添加Runnables时它的价值)?当我执行 lambda 表达式时,这应该无关紧要,对吧?我的意思是,好吧,在具有增强的for循环的版本中,我也稍后执行Runnables,但正确/不同的值是在更早(添加Runnables时)捕获的。names[ind[0]]

换句话说,为什么Java在捕获值时并不总是具有按值/快照语义(如果我可以这样说的话)?难道它不是更干净,更有意义吗?


答案 1

换句话说,为什么Java在捕获值时并不总是具有按值/快照语义(如果我可以这样说的话)?难道它不是更干净,更有意义吗?

Java lambdas确实具有按值捕获的语义。

当您执行以下操作时:

runners.add(() -> System.out.println(names[ind[0]]));

此处的 lambda 捕获两个:和 。这两个值恰好是对象引用,但对象引用是一个类似于“3”的值。当捕获的对象引用引用可变对象时,事情可能会令人困惑,因为它们在 lambda 捕获期间的状态和在 lambda 调用期间的状态可能不同。具体来说,引用的数组确实以这种方式更改,这是问题的原因。indnamesind

lambda 未捕获的是表达式 的值。相反,它会捕获引用 ,并在调用 lambda 时执行取消引用 。Lambdas从其词法封闭范围中关闭值; 是词法封闭作用域中的值,但不是。我们在评估时捕获并进一步使用它。ind[0]indind[0]indind[0]ind

您在这里以某种方式期望 lambda 将对整个堆中的所有对象进行完整快照,这些对象可通过捕获的引用访问,但这不是它的工作方式 - 这也没有多大意义。

摘要:Lambda 按值捕获所有捕获的参数(包括对象引用)。但是对象引用和它所引用的对象不是一回事。如果您捕获对可变对象的引用,则在调用 lambda 时,该对象的状态可能已更改。


答案 2

我会说代码完全符合你告诉他要做的事情。

您创建“Runnable”,它将名称打印在 从 的位置。但该表达式将在第二个 for 循环中计算。在这一点上.因此,表达式打印 .ind[0]ind[0]=2"Mary"