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

pet*_*rov 25 java lambda closures java-8

我想我已经在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();
        }

    }

}
Run Code Online (Sandbox Code Playgroud)

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

for (String name : names){
    runners.add(() -> System.out.println(name));
}
Run Code Online (Sandbox Code Playgroud)

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

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

编辑:

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

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

Bri*_*etz 44

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

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

当你这样做时:

runners.add(() -> System.out.println(names[ind[0]]));
Run Code Online (Sandbox Code Playgroud)

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

lambda没有捕获的是表达式的值ind[0].相反,它捕获引用ind,并且在调用 lambda时执行解除引用ind[0].Lambdas从其词汇封闭范围内接近价值; ind是词法封闭范围内的值,但ind[0]不是.我们ind在评估时进一步捕获并使用它.

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

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

  • 我认为这是最清楚的解释.现在我真的有这种感觉,我深入了解它.现在它非常有意义.稍后我会再读几遍.非常感谢. (2认同)
  • @ peter.petrov是的,确切地说.就像对象引用(以及其他所有内容)通过值传递一样,对象引用(以及其他所有内容)都是通过值捕获的. (2认同)
  • @ user2357112只要捕获的变量必须是"final",那么变量本身或它们的当前值是否被捕获没有区别. (2认同)

Cla*_*ayn 14

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

您创建"Runnable",在该位置打印名称ind[0].但是这个表达式会在你的第二个for循环中得到评估.在这一点上ind[0]=2.所以表达式打印"Mary".


Era*_*ran 10

创建lambda表达式时,不执行它.当你调用run每个for循环时,它只在你的第二个for循环中执行Runnable.

当执行第二个循环时,ind[0]包含2,因此在执行所有run方法时打印"Mary" .

编辑:

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

StringBuilder[] names = { new StringBuilder().append ("Peter"), new StringBuilder().append ("Paul"), new StringBuilder().append ("Mary") };
List<Runnable> runners = new ArrayList<>();

for (StringBuilder name : names){
  runners.add(() -> System.out.println(name));
  name.setLength (0);
  name.append ("Mary");
}

for (int k=0; k<runners.size(); k++){
  runners.get(k).run();
}
Run Code Online (Sandbox Code Playgroud)

输出:

Mary
Mary
Mary
Run Code Online (Sandbox Code Playgroud)

  • @ peter.petrov为什么要这样?它与使用`new Runnable(){System.out.println(names [ind [0]]);};`相同,这应该导致相同的输出 (3认同)

sti*_*ike 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]));
    }
Run Code Online (Sandbox Code Playgroud)

这将根据需要打印所有名称

或在ind当地宣布

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

  • 如果他甚至在当地宣布它将工作 (2认同)