在这种情况下,为什么我不能在lambda中引用变量?

ski*_*iwi 13 java lambda java-8

我有以下代码,它有点抽象我在Java程序中的实际实现:

BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in));
String line;
while ((line = bufferedReader.readLine()) != null) {
    String lineReference = line;
    runLater(() -> consumeString(lineReference));
}
Run Code Online (Sandbox Code Playgroud)

这里我需要使用lambda表达式的引用副本,当我尝试使用时line我得到:

从lambda表达式引用的局部变量必须是最终的或有效的final

对我来说这似乎很尴尬,因为我所做的就是获取对象的新引用,这是编译器也可以自己解决的问题.

所以,我要说line有效决赛在这里,因为它只是变得回路中的分配和其他地方.

任何人都可以对此有所了解并解释为什么这里需要它以及为什么编译无法解决它?

Boa*_*ann 25

所以,我要说line有效决赛在这里,因为它只是变得回路中的分配和其他地方.

不,它不是最终的,因为在变量的生命周期中,它会在每次循环迭代时被赋予一个新值.这与决赛完全相反.

我得到:'从lambda表达式引用的局部变量必须是最终的或有效的最终'.这对我来说似乎很尴尬.

考虑一下:你将lambda传递给了runLater(...).当lambda最终执行时,它line应该使用哪个值?创建lambda时的值,或lambda执行时的值?

规则是lambda(看起来)在lambda执行时使用当前值.他们没有(似乎)创建变量的副本.现在,这条规则在实践中如何实施?

  • 如果line是静态字段,则很容易,因为没有lambda捕获的状态.lambda可以在需要时读取字段的当前值,就像任何其他代码一样.

  • 如果line是一个实例字段,这也是相当容易的.lambda可以捕获每个lambda对象中私有隐藏字段中对象的引用,并line通过它访问该字段.

  • 如果line是方法中的局部变量(就像在您的示例中那样),这突然变得容易.在实现级别,lambda表达式采用完全不同的方法,并且外部代码没有简单的方法来共享对仅存在于一个方法中的变量的访问.

要启用对局部变量的访问,编译器必须将变量装入一些隐藏的,可变的holder对象(例如1元素数组),以便可以从封闭方法和lambda引用holder对象,他们都可以访问变量.

尽管该解决方案在技术上可行,但由于一系列原因,它所实现的行为是不可取的.分配持有者对象会给局部变量带来不自然的性能特征,这在阅读代码时并不明显.(仅仅定义一个使用局部变量的lambda会使整个方法中的变量变慢.)更糟糕的是,它会在其他简单的代码中引入细微的竞争条件,具体取决于执行lambda的时间.在您的示例中,当lambda执行时,可能发生任意数量的循环迭代,或者方法可能已返回,因此line变量可以具有任何值或没有定义的值,并且几乎肯定不会具有您想要的值.所以在实践中你仍然需要单独的,不变的lineReference变量!唯一的区别是编译器不会要求你这样做,因此它允许你编写损坏的代码.由于lambda最终可以在不同的线程上执行,这也会为局部变量引入细微的并发和线程可见性复杂性,这需要语言允许volatile修饰符对局部变量,以及其他麻烦.

因此,对于lambda来说,当前变量的当前变化值会引入很多大惊小怪(并且没有优势,因为如果你需要的话你可以手动执行可变持有者技巧).相反,语言通过简单地要求变量final(或实际上是最终的)来对整个kerfuffle说不.这样,lambda可以在lambda创建时捕获局部变量的值,并且它不需要担心检测到更改,因为它知道不能有任何更改.

这是编译器本身也可以解决的问题

它确实搞清楚了,这就是为什么它不允许它.该lineReference变量绝对没有对编译器有任何好处,它可以很容易地捕获line在每个lambda对象的创建时间在lambda中使用的当前值.但是由于lambda不会检测到变量的变化(由于上面解释的原因,这将是不切实际和不可取的),捕获字段和捕获本地人之间的细微差别将令人困惑."最终或有效最终"规则是为了程序员的利益:它可以防止您想知道为什么变量的变化不会出现在lambda中,因为它根本不会改变它们.以下是没有该规则会发生什么的示例:

String field = "A";
void foo() {
    String local = "A";
    Runnable r = () -> System.out.println(field + local);
    field = "B";
    local = "B";
    r.run(); // output: "BA"
}
Run Code Online (Sandbox Code Playgroud)

如果lambda中引用的任何局部变量(有效)是最终的,那么这种混淆就会消失.

在你的代码中,lineReference 实际上是最终的.它的值在其生命周期中只分配一次,然后在每次循环迭代结束时超出范围,这就是为什么你可以在lambda中使用它.

通过line在循环体内声明,可以有一种替代的循环安排:

for (;;) {
    String line = bufferedReader.readLine();
    if (line == null) break;
    runLater(() -> consumeString(line));
}
Run Code Online (Sandbox Code Playgroud)

这是允许的,因为line现在在每次循环迭代结束时超出范围.每次迭代都有一个新变量,只分配一次.(但是,在较低的级别,变量仍然存储在同一个CPU寄存器中,因此不必重复"创建"和"销毁".我的意思是,在内部声明变量没有额外的成本像这样的循环,所以没关系.)


注意:所有这些并不是lambdas独有的.它也同样适用于在方法中以词法方式声明的任何类,lambdas从中继承了规则.

注2:可以说,如果lambda遵循始终捕获它们在lambda创建时使用的变量值的规则,那么lambdas会更简单.然后,字段和本地之间的行为没有区别,也不需要"最终或有效最终"规则,因为已经确定lambda不会看到在lambda创建时间之后所做的更改.但是这条规则会有自己的规则.作为一个示例,对于x在lambda内访问的实例字段,在读取x(捕获最终值x)和this.x(捕获其最终值this,看到其字段x改变)的行为之间将存在差异.语言设计很难.