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

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/24742584/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-14 14:37:29  来源:igfitidea点击:

Why can I not reference the variable from within a lambda in this case?

javalambdajava-8

提问by skiwi

I have got the following code, which is somewhat abstracted from a real implementation I had in a Java program:

我得到了以下代码,它是从我在 Java 程序中的实际实现中抽象出来的:

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

Here I need to use a reference copy for the lambda expression, when I try to use lineI get:

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

Local variables referenced from a lambda expression must be final or effectively final

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

It seems rather awkward to me, as all I do to fix it is obtain a new reference to the object, this is something the compiler could also figure out by itself.

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

So I would say lineis effectively finalhere, as it only gets the assignment in the loop and nowhere else.

所以我会说这里实际上line最终的,因为它只在循环中获得分配而没有其他地方。

Could anyone shed some more light on this and explain why exactly it is needed here and why the compile cannot fix it?

任何人都可以对此进行更多说明并解释为什么这里需要它以及为什么编译无法修复它?

采纳答案by Boann

So I would say lineis effectively finalhere, as it only gets the assignment in the loop and nowhere else.

所以我会说这里实际上line最终的,因为它只在循环中获得分配而没有其他地方。

No, it's not final because during the variable's lifetime it is getting assigned a new value on every loop iteration. This is the complete opposite of final.

不,它不是最终的,因为在变量的生命周期内,每次循环迭代都会为它分配一个新值。这与 final 完全相反。

I get: 'Local variables referenced from a lambda expression must be final or effectively final'. It seems rather awkward to me.

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

Consider this: You're passing the lambda to runLater(...). When the lambda finally executes, which value of lineshould it use? The value it had when the lambda was created, or the value it had when the lambda executed?

考虑一下:您将 lambda 传递给runLater(...). 当 lambda 最终执行时,它line应该使用哪个值?它在创建 lambda 时拥有的值,还是在 lambda 执行时拥有的值?

The rule is that lambdas (appear to) use the current value at time of lambda execution. They do not (appear to) create a copy of the variable. Now, how is this rule implemented in practice?

规则是 lambdas(似乎)在 lambda 执行时使用当前值。他们不会(似乎)创建变量的副本。现在,这条规则在实践中是如何实施的?

  • If lineis a static field, it's easy because there is no state for the lambda to capture. The lambda can read the current value of the field whenever it needs to, just as any other code can.

  • If lineis an instance field, that's also fairlyeasy. The lambda can capture the reference to the object in a private hidden field in each lambda object, and access the linefield through that.

  • If lineis a local variable within a method (as it is in your example), this is suddenly noteasy. At an implementation level, the lambda expression is in a completely different method, and there is no easy way for outside code to share access to the variable which only exists within the one method.

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

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

  • Ifline是方法中的局部变量(如您的示例中所示),这突然变得容易。在实现级别,lambda 表达式在一个完全不同的方法中,外部代码没有简单的方法来共享对仅存在于一个方法中的变量的访问。

To enable access to the local variable, the compiler would have to box the variable into some hidden, mutable holder object (such as a 1-element array) so that the holder object could be referenced from both the enclosing method and the lambda, giving them both access to the variable within.

为了能够访问局部变量,编译器必须将变量装箱到一些隐藏的、可变的持有者对象(例如 1 元素数组)中,以便可以从封闭方法和 lambda 中引用持有者对象,给出它们都可以访问内部的变量。

Although that solution would technically work, the behavior it achieves would be undesirable for a bundle of reasons. Allocating the holder object would give local variables an unnatural performance characteristic which would not be obvious from reading the code. (Merely defining a lambda that used a local variable would make the variable slower throughout the method.) Worse than that, it would introduce subtle race conditions into otherwise simple code, depending on when the lambda is executed. In your example, by the time the lambda executes, any number of loop iterations could have happened, or the method might have returned, so the linevariable could have any value or no defined value, and almost certainly wouldn't have the value you wanted. So in practice you'd stillneed the separate, unchanging lineReferencevariable! The only difference is that the compiler wouldn't require you do to that, so it would allow you to write broken code. Since the lambda could ultimately execute on a different thread, this would also introduce subtle concurrency and thread visibility complexity to local variables, which would require the language to allow the volatilemodifier on local variables, and other bother.

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

So, for the lambda to see the current changing values of local variables would introduce a lot of fuss (and no advantages since you can do the mutable holder trick manuallyif you ever need to). Instead, the language says no to the whole kerfuffle by simply demanding that the variable be final(or effectively final). That way, the lambda can capture the value of the local variable at lambda creation time, and it doesn't need to worry about detecting changes because it knows there can't be any.

因此,要让 lambda 看到局部变量的当前变化值会带来很多麻烦(并且没有优势,因为您可以在需要时手动执行可变持有者技巧)。相反,该语言通过简单地要求变量是final(或实际上是最终的)来对整个混乱说不。这样,lambda 可以在创建 lambda 时捕获局部变量的值,并且不需要担心检测更改,因为它知道不可能有任何更改。

This is something the compiler could also figure out by itself

这是编译器也可以自己解决的问题

It did figure it out, which is why it disallows it. The lineReferencevariable is of absolutely no benefit to the compiler, which could easily capture the current value of linefor use in the lambda at each lambda object's creation time. But since the lambda wouldn't detect changes to the variable (which would be impractical and undesirable for the reasons explained above), the subtle difference between capture of fields and capture of locals would be confusing. The "final or effectively final" rule is for the programmer's benefit: it prevents you from wondering why changes to a variable don't appear within a lambda, by preventing you from changing them at all. Here's an example of what would happen without that rule:

它确实弄清楚了,这就是为什么它不允许它。该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"
}

That confusion goes away if any local variables referenced within the lambda are (effectively) final.

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

In your code, lineReferenceiseffectively final. Its value is assigned exactly onceduring its lifetime, before it goes out of scope at the end of each loop iteration, which is why you can use it in the lambda.

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

There is an alternative arrangement of your loop possible by declaring lineinside the loop body:

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

for (;;) {
    String line = bufferedReader.readLine();
    if (line == null) break;
    runLater(() -> consumeString(line));
}

This is allowed because linenow goes out of scope at the end of each loop iteration. Each iteration effectively has a fresh variable, assigned exactly once. (However, at a low level the variable is still stored in the same CPU register, so it's not like it has to be repeatedly "created" and "destroyed". What I mean is, there is happily no extra cost to declaring variables inside a loop like this, so it's fine.)

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



Note: All this is not unique to lambdas. It also applies identically to any classes declared lexically inside the method, from which lambdas inherited the rules.

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

Note 2: It could be argued that lambdas would be simpler if they followed the rule of always capturing the values of variables they use at lambda creation time. Then there would be no difference in behavior between fields and locals, and no need for the "final or effectively final" rule because it would be well-established that lambdas don't see changes made after lambda creation time. But this rule would have its own uglinesses. As one example, for an instance field xaccessed within a lambda, there would be a difference between the behavior of reading x(capturing final value of x) and this.x(capturing final value of this, seeing its field xchanging). Language design is hard.

注 2:如果 lambda 遵循始终捕获它们在 lambda 创建时使用的变量的值的规则,则可能会认为 lambda 会更简单。那么字段和局部变量之间的行为将没有区别,并且不需要“最终或有效最终”规则,因为 lambda 不会看到在 lambda 创建时间之后所做的更改。但这条规则也有它自己的丑陋之处。例如,对于x在 lambda 中访问的实例字段,读取行为x(捕获 的最终值x)和this.x(捕获 的最终值this,看到其字段x发生变化)之间存在差异。语言设计很难。

回答by Eran

If you would use lineinstead of lineReferencein the lambda expression, you would be passing to your runLatermethod a lambda expression that would execute consumeStringon a String referred by line.

如果您将在 lambda 表达式中使用line而不是lineReference,您将向您的runLater方法传递一个 lambda 表达式,该表达式将consumeString在由 引用的字符串上执行line

But linekeeps changing as you assign new lines to it. When you finally execute the method of the functional interface returned by the lambda expression, only then will it get the current value of lineand use it in the call to consumeString. At this point the value of linewould not be the same as it was when you passed the lambda expression to the runLatermethod.

但是line随着您为其分配新行而不断变化。当您最终执行由 lambda 表达式返回的函数式接口的方法时,它才会获取 的当前值line并在对 的调用中使用它consumeString。此时, 的值line将与将 lambda 表达式传递给runLater方法时的值不同。