当我将 lambda 表达式作为参数传递时,它怎么可能访问此范围内的其他变量?

How is it possible that when I pass a lambda expression as a parameter it can access other variables in this scope?

public class ArraysDemo {

    public static void main(String[] args) {

          int[] a = {0, 2, 4, 6, 8};
          int[] b = {10, 12, 14, 16, 18};
          Arrays.setAll(a, i -> b[i]+1);
          System.out.println(Arrays.toString(a));
    }  
}

输出:[11, 13, 15, 17, 19]

使用的setAll()函数的来源如下:

public static void setAll(int[] array, IntUnaryOperator generator) {
        Objects.requireNonNull(generator);
        for (int i = 0; i < array.length; i++)
            array[i] = generator.applyAsInt(i);
}

IntUnaryOperator 是一个功能接口,这是其来源的一部分:

public interface IntUnaryOperator {
    int applyAsInt(int operand);
    // rest methods are omitted
}

如果我错了请纠正我,但我对 Java 中的 lambda 表达式的理解是,当我将 lambda 表达式作为参数传递给 setAll() 方法时,匿名 class 实现了 IntUnaryOperator 接口,创建并调用 generator。而 lambda 表达式本质上是 applyAsInt() 方法的一个实现,所以我相信它会转化为类似这样的东西:

int applyAsInt(int operand){
    return b[operand]+1;
}

它可以访问 operand 对我来说很有意义,因为它作为参数传递给 array[i] = generator.applyAsInt(i); 但是,我不明白它如何操纵 b - 它没有作为参数传递,那么它怎么可能被引用呢?我错过了什么?

原因是 b 有效最终

Instance and static variables may be used and changed without restriction in the body of a lambda. The use of local variables, however, is more restricted: capture of local variables is not allowed unless they are effectively final, a concept introduced in Java 8. Informally, a local variable is effectively final if its initial value is never changed (including within the body of a lambda expression)—in other words, declaring it final would not cause a compilation failure. The concept of effective finality does not introduce any new semantics to Java; it is simply a slightly less verbose way of defining final variables.

来自http://www.lambdafaq.org/can-lambda-expressions-use-variables-from-their-environment/

      Arrays.setAll(a, i -> b[i]+1);

相当于效果:

      Arrays.setAll(a, new IntUnaryOperator() {

              private int[] b$ = b;

              @Override
              public int applyAsInt(int i);
                  return b$[i]+1;
              }
          });

那是一个与原来b同名同值的变量b会被保留。需要此副本,因为原始变量(或新变量)的寿命可能比另一个更有限。例如作为局部变量。

为了隐藏现在有两个变量,要求不再将它们分配给 ("effectively final"),这会导致 "same" 变量的值不同。

即使没有 lambda 表达式,例如使用匿名内部 classes,您可以 捕获 周围上下文的值,即

int[] a = {0, 2, 4, 6, 8};
int[] b = {10, 12, 14, 16, 18};
Arrays.setAll(a, new IntUnaryOperator() {
    public int applyAsInt(int i) {
        return b[i]+1;
    }
});

但也有一些差异,虽然与您的问题无关。当使用匿名内部 class 时,关键字 thissuper 引用内部 class 的实例,而在 lambda 表达式中,它们的含义与周围环境。此外,当内部 class 访问周围 class 的 private 成员时,编译器将插入 helper 方法来执行访问,而 lambda 表达式可以访问包含 [=45] 的成员=]自然。

为了实现这一点,您的 lambda 表达式 i -> b[i]+1 的代码将被编译为 您的 class 的合成方法,其形式为:

private static int lambda$main[=11=](int[] b, int i) {
    return b[i]+1;
}

因为它是您 class 中的一个方法,它可以访问所有成员。

所以运行时生成的class相当于

final class ArraysDemo$Lambda implements IntUnaryOperator {
    final int[] b;
    ArraysDemo$Lambda(int[] b) {
        this.b = b;
    }
    public int applyAsInt(int i) {
        return ArraysDemo.lambda$main[=12=](b, i);
    }
}

把重点放在等价这个词上,并不是说一定要完全这样(更何况,普通的Javaclasses 无法访问 private 方法 ArraysDemo.lambda$main[=18=]).


但是这些只是技术细节,最重要的关键点是lambda表达式可以访问有效的final局部变量以及它们包含的成员class和编译器和运行时环境将确保它工作。您可以简单地认为它们 函数 在定义它们的上下文中被评估。