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

时间:2014-07-14 17:47:58

标签: 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));
}

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

  

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

对我而言似乎相当尴尬,因为我所做的就是获取对象的新引用,这是编译器本身也可以解决的问题。

所以我会说line 实际上是 ,因为它只能在循环中获得分配,而不是其他地方。

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

2 个答案:

答案 0 :(得分:26)

  

所以我会说line 实际上是 ,因为它只能在循环中获得分配,而不是其他地方。

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

  

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

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

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

  • 如果line是一个静态字段,那很简单,因为没有lambda捕获的状态。 lambda可以随时读取字段的当前值,就像任何其他代码一样。

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

  • 如果line是方法中的局部变量(就像在您的示例中那样),则突然 。在实现级别,lambda表达式is in a completely different method,并且外部代码没有简单的方法来共享对仅存在于一个方法中的变量的访问。

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

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

因此,对于lambda来说,当前变量的当前变化值会引入很多大惊小怪(如果你需要的话,自you can do the mutable holder trick manually起没有任何优势)。相反,语言通过简单地要求变量为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"
}

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

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

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

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

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


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

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

答案 1 :(得分:1)

如果您在lambda表达式中使用line而不是lineReference,那么您将向runLater方法传递一个lambda表达式,该表达式将对字符串执行consumeStringline引用。

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