所以我想说这里实际上是最终的,因为它只在循环中得到分配,而不是其他地方。line
不,它不是最终的,因为在变量的生存期内,它会在每次循环迭代时被分配一个新值。这与最终版完全相反。
我得到:“从lambda表达式引用的局部变量必须是最终的或有效的最终的”。这对我来说似乎很尴尬。
请考虑以下情况:您将 lambda 传递给 。当 lambda 最终执行时,它应该使用哪个值?创建 lambda 时它拥有的值,还是执行 lambda 时它拥有的值?runLater(...)
line
规则是 lambda(似乎)在 lambda 执行时使用当前值。它们不会(似乎)创建变量的副本。现在,这条规则在实践中是如何实施的呢?
如果 是静态字段,则很容易,因为 lambda 没有要捕获的状态。lambda 可以在需要时读取字段的当前值,就像任何其他代码一样。line
如果 是实例字段,那也相当容易。lambda 可以在每个 lambda 对象的私有隐藏字段中捕获对对象的引用,并通过该字段访问该字段。line
line
如果 是方法中的局部变量(如示例中所示),则这突然变得不容易了。在实现级别,lambda 表达式采用完全不同的方法,外部代码没有简单的方法来共享对仅存在于一个方法中的变量的访问权限。line
若要启用对局部变量的访问,编译器必须将变量框入某个隐藏的可变持有者对象(例如 1 元素数组)中,以便可以从封闭方法和 lambda 引用持有者对象,从而使它们都能访问其中的变量。
尽管该解决方案在技术上是可行的,但由于一系列原因,它实现的行为是不可取的。分配持有者对象会给局部变量一个不自然的性能特征,这在阅读代码时并不明显。(仅定义一个使用局部变量的 lambda 会使整个方法中的变量变慢。更糟糕的是,它会在其他简单的代码中引入微妙的竞争条件,具体取决于执行 lambda 的时间。在您的示例中,当 lambda 执行时,可能会发生任意数量的循环迭代,或者方法可能已返回,因此变量可能具有任何值或没有定义的值,并且几乎肯定不会具有所需的值。因此,在实践中,您仍然需要单独的,不变的变量!唯一的区别是编译器不需要你这样做,所以它允许你编写损坏的代码。由于 lambda 最终可以在不同的线程上执行,因此这也会给局部变量带来微妙的并发性和线程可见性复杂性,这将要求语言允许对局部变量进行修饰符,以及其他麻烦。line
lineReference
volatile
因此,对于lambda来说,要查看局部变量的当前变化值会带来很多麻烦(并且没有优势,因为如果需要,您可以手动执行可变持有者技巧)。相反,该语言通过简单地要求变量是(或实际上是最终的)来对整个混乱说不。这样,lambda 可以在 lambda 创建时捕获局部变量的值,并且无需担心检测更改,因为它知道不可能有任何更改。final
这是编译器也可以自己弄清楚的。
它确实弄清楚了,这就是为什么它不允许它。该变量对编译器绝对没有好处,编译器可以在每个lambda对象的创建时轻松捕获用于lambda的当前值。但是,由于 lambda 不会检测变量的变化(由于上述原因,这是不切实际和不可取的),因此字段捕获和局部捕获之间的细微差别将令人困惑。“最终或实际上是最终的”规则是为了程序员的利益:它通过阻止您更改变量来防止您想知道为什么对变量的更改不会出现在 lambda 中。下面是一个示例,说明如果没有该规则会发生什么情况:lineReference
line
String field = "A";
void foo() {
String local = "A";
Runnable r = () -> System.out.println(field + local);
field = "B";
local = "B";
r.run(); // output: "BA"
}
如果 lambda 中引用的任何局部变量(实际上)是最终的,那么这种混淆就会消失。
在你的代码中,实际上是最终的。它的值在其生存期内只分配一次,在每次循环迭代结束时超出范围之前,这就是您可以在 lambda 中使用它的原因。lineReference
可以通过在循环体内部声明来对循环进行另一种排列:line
for (;;) {
String line = bufferedReader.readLine();
if (line == null) break;
runLater(() -> consumeString(line));
}
这是允许的,因为现在在每个循环迭代结束时超出范围。每次迭代实际上都有一个新变量,只分配一次。(但是,在较低级别,变量仍存储在同一个CPU寄存器中,因此不必重复“创建”和“销毁”。我的意思是,很高兴在这样的循环中声明变量没有额外的成本,所以没关系。line
注意:所有这些都不是 lambdas 所独有的。它同样适用于方法内以词法声明的任何类,lambdas 从这些类继承规则。
注 2:可以说,如果 lambda 遵循始终捕获它们在 lambda 创建时使用的变量值的规则,则 lambda 会更简单。然后,字段和局部变量之间的行为就没有区别,也不需要“最终或实际上是最终”规则,因为很明显,lambda 在 lambda 创建后看不到所做的更改。但这条规则有其自身的丑陋。例如,对于在 lambda 中访问的实例字段,读取行为(捕获 的最终值 )和 (捕获 的最终值,看到其字段发生变化)之间存在差异。语言设计很难。x
x
x
this.x
this
x