gpt4 book ai didi

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

转载 作者:行者123 更新时间:2023-12-01 19:42:13 28 4
gpt4 key购买 nike

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

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

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

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



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

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

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

最佳答案

So I would say line is effectively final here, as it only gets the assignment in the loop and nowhere else.



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

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



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

规则是 lambdas(似乎)在 lambda 执行时使用当前值。他们不会(似乎)创建变量的副本。现在,这条规则在实践中是如何实现的?
  • line是一个静态字段,这很容易,因为 lambda 没有要捕获的状态。 lambda 可以在需要时读取字段的当前值,就像任何其他代码一样。
  • line是一个实例字段,这也很容易。 lambda 可以在每个 lambda 对象中的私有(private)隐藏字段中捕获对对象的引用,并访问 line场通过。
  • line是方法中的局部变量(如您的示例中所示),这突然变得不容易。在实现级别,lambda 表达式 is in a completely different method ,并且外部代码没有简单的方法来共享对仅存在于一种方法中的变量的访问。

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

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

    因此,要让 lambda 看到局部变量的当前变化值会带来很多麻烦(如果您需要的话,自 you can do the mutable holder trick manually 以来没有任何优势)。相反,该语言通过简单地要求变量为 final 来拒绝整个困惑局面。 (或有效地最终)。这样,lambda 可以在创建 lambda 时捕获局部变量的值,并且不需要担心检测更改,因为它知道不可能有任何更改。

    This is something the compiler could also figure out by itself



    它确实弄清楚了,这就是为什么它不允许它。 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 寄存器中,因此不必重复“创建”和“销毁”。我的意思是,在内部声明变量很高兴没有额外成本像这样的循环,所以没问题。)

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

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

    关于java - 在这种情况下,为什么我不能从 lambda 中引用变量?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/24742584/

    28 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com