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

时间:2015-10-12 10:55:26

标签: java lambda java-8 closures

我认为我在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]));
}

修改

我的观点是:为什么不捕获names[ind[0]]的值(我添加Runnables时它的值)?我执行lambda表达式时无所谓,对吧?我的意思是,好吧,在具有增强的for循环的版本中,我也稍后执行Runnables但是之前捕获了正确/不同的值(添加Runnables时)。

换句话说,为什么Java在捕获值时总是没有这个按值/快照语义(如果我可以这样说)?它会更清洁,更有意义吗?

6 个答案:

答案 0 :(得分:44)

  

换句话说,为什么Java在捕获值时总是没有这个值/快照语义(如果我可以这样说)?它会更清洁,更有意义吗?

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

当你这样做时:

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

此处的lambda捕获两个indnames。这两个值恰好都是对象引用,但对象引用是一个类似'3'的值。当捕获的对象引用引用可变对象时,这可能会使事情变得混乱,因为它们在lambda捕获期间的状态和在lambda调用期间的状态可能不同。具体来说,ind引用的数组确实会以这种方式发生变化,这是导致问题的原因。

lambda没有捕获的是表达式ind[0]的值。相反,它捕获引用ind,并且当lambda 被调用时,执行解除引用ind[0]。 Lambdas从其词汇封闭范围内接近价值; ind是词法封闭范围中的值,但ind[0]不是。ind。我们捕获 public EmListView() { Style defaultStyle = (Style)Application.Current.TryFindResource(typeof(EmListView)); if (defaultStyle != null) { this.OverridesDefaultStyle = true; this.Style = defaultStyle; } } 并在评估时进一步使用它。

你在某种程度上期待lambda将完成整个堆中可通过捕获的引用访问的所有对象的完整快照,但这不是它的工作方式 - 这也不会有很大意义。

摘要:Lambdas按值捕获所有捕获的参数 - 包括对象引用。但是对象引用和它引用的对象不是一回事。如果捕获对可变对象的引用,则对象的状态可能在调用lambda时发生了更改。

答案 1 :(得分:14)

我会说代码完全按照你的要求去做。

您创建&#34; Runnable&#34;在ind[0]的位置打印名称。 但是这个表达式会在你的第二个for循环中得到评估。此时ind[0]=2。因此表达式打印"Mary"

答案 2 :(得分:10)

创建lambda表达式时,不执行它。 当您调用每个CREATE TABLE database1.employees AS SELECT * FROM database2.employees; 的{​​{1}}方法时,它仅在您的第二个for循环中执行。

当执行第二个循环时,run包含2,因此&#34; Mary&#34;在执行所有Runnable方法时打印。

编辑:

增强的for循环行为有所不同,因为在该片段中,lambda表达式保存对ind[0]实例的引用,run是不可变的。如果将String更改为String,则可以使用增强的for循环构建示例,该示例还会打印所引用实例的最终值:

String

输出:

StringBuilder

答案 3 :(得分:6)

它工作正常..在调用println方法时,ind[0]2,因为您正在使用公共变量并在函数调用之前增加它的值。因此它将始终打印Mary。您可以执行以下操作来检查

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

这将打印所需的所有名称

或在本地声明ind

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

答案 4 :(得分:4)

解释此行为的另一种方法是查看lambda表达式替换的内容:

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

的语法糖
runers.add(new Runnable() {
    final String[] names_inner = names;
    final int[] ind_inner = ind;
    public void run() {
        System.out.println(names_inner[ind_inner[0]]));
    }
});

变成

// yes, this is inside the main method's body
class Test008$1 implements Runnable {
    final String[] names_inner;
    final int[] ind_inner;

    private Test008$1(String[] n, int[] i) {
        names_inner = n; ind_inner = i;
    }

    public void run() {
        System.out.println(names_inner[ind_inner[0]]));
    }
}
runers.add(new Test008$1(names,ind));

(生成的东西的名称并不重要。这个美元符号只是Java中生成的方法/类名称中经常使用的字符 - 这就是它被保留的原因。 )

添加了两个内部字段,以便Runnable中的代码可以看到它外面的变量;如果在外部代码(主要方法)中将namesind定义为final,则内部字段不是必需的。正如Brian Goetz在他的回答中解释的那样,变量可以通过值传递。

数组就像对象一样,如果将它们传递给修改它们的方法,它们也会在原始位置被修改;其余的只是OOP基础知识。所以这实际上是正确的行为。

答案 5 :(得分:2)

好吧,在lambda之前,Java曾经有一个更严格的规则来捕获匿名类中的本地/参数变量。也就是说,只允许捕获final个变量。我相信这是为了防止并发混淆 - 最终变量不会改变。因此,如果您将以这种方式创建的某些实例传递到任务队列并让它由另一个线程执行,至少您知道这两个线程共享相同的值。 Why are only final variables accessible in anonymous class?

解释了更深层次的原因

现在,使用lambdas和Java 8,您可以捕获有效最终的变量&#34;,这些变量未被声明为final,但编译器根据如何将其视为最终变量它的读写。基本上,如果你将final关键字添加到有效最终变量的声明中,编译器就不会抱怨它。