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

Java中的闭包-捕获值-为什么会出现这种意外结果?,java,lambda,java-8,closures,Java,Lambda,Java 8,Closures,我想我在JavaScript中遇到过这种典型情况 通常程序员希望下面的代码打印“Peter”、“Paul”、“Mary” 但事实并非如此。有人能解释一下为什么它在Java中是这样工作的吗 这段Java8代码编译OK并打印3次“Mary” 我想这是一个如何在深层实施的问题 但是这不表示底层实现错误吗 import java.util.List; import java.util.ArrayList; public class Test008 { public static void m

我想我在JavaScript中遇到过这种典型情况

通常程序员希望下面的代码打印“Peter”、“Paul”、“Mary”

但事实并非如此。有人能解释一下为什么它在Java中是这样工作的吗

这段Java8代码编译OK并打印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循环(在添加runnable时),那么我会得到一个编译错误(这很有意义,因为变量
I
不是final或者实际上不是final)

for(inti=0;iSystem.out.println(names[i]);
}
编辑:

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


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

创建lambda表达式时,您没有执行它。 当您调用每个
Runnable
run
方法时,它只在第二个for循环中执行

执行第二个循环时,
ind[0]
包含2,因此在执行所有
run
方法时都会打印“Mary”

编辑:

增强型for循环的行为不同,因为在该代码段中,lambda表达式包含对
String
实例的引用,并且
String
是不可变的。如果将
String
更改为
StringBuilder
,则可以构建一个具有增强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();
}

我想说代码完全按照你告诉他的去做

您可以创建“Runnable”,在
ind[0]
位置打印名称。
但该表达式将在第二个for循环中求值。此时,
ind[0]=2
。因此,表达式将打印“Mary”

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

    for (int i = 0; i < names.length; i++) {
        final int j = i;
        runners.add(() -> System.out.println(names[j]));
    }
换句话说,为什么Java在捕获值时不总是具有按值/快照语义(如果我可以这样说的话)?这不是更干净、更有意义吗


Java lambda确实具有按值捕获语义

当您这样做时:

runners.add(() -> System.out.println(names[ind[0]]));
这里的lambda捕获两个值:
ind
names
。这两个值恰好都是对象引用,但对象引用是与“3”类似的值。当一个捕获的对象引用引用一个可变对象时,这就是事情变得混乱的地方,因为它们在lambda捕获期间的状态和在lambda调用期间的状态可能不同。具体来说,
ind
引用的数组确实会以这种方式更改,这就是问题的原因

lambda没有捕获的是表达式
ind[0]
的值。相反,它捕获引用
ind
,并在调用lambda时执行解引用
ind[0]
。lambda从其词汇封闭范围关闭值
ind
是词汇范围内的一个值,但
ind[0]
不是。我们捕获
ind
,并在评估时进一步使用它

在这里,您可能希望lambda能够对整个堆中通过捕获的引用可以访问的所有对象进行完整的快照,但这不是它的工作方式,也没有什么意义


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

在lambda之前,Java曾经有一个更严格的规则来捕获匿名类中的局部/参数变量。也就是说,只允许捕获
final
变量。我相信这是为了防止并发混淆——最终变量不会改变。因此,如果将以这种方式创建的一些实例传递到任务队列中,并由另一个线程执行,至少您知道这两个线程共享相同的值。一个更深层次的原因是


现在,使用lambdas和java8,您可以捕获“有效最终”的变量,这些变量不是声明为
final
,而是编译器根据其读写方式将其视为最终变量。基本上,如果将final关键字添加到有效final变量的声明中,编译器不会对此抱怨。

解释此行为的另一种方法是查看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内部的代码可以看到其外部的变量;如果
名称
ind
在外部代码(main方法)中定义为final,则不需要内部字段。这
runners.add(() -> System.out.println(names[ind[0]]));
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));