BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in)); String line; while ((line = bufferedReader.readLine()) != null) { String lineReference = line; runLater(() -> consumeString(lineReference)); }
在这里我需要使用lambda表达式的引用副本,当我尝试使用行我得到:
Local variables referenced from a lambda expression must be final or effectively final
对我来说似乎相当尴尬,因为我所做的一切都是为了获得对该对象的新引用,这是编译器本身也可以理解的东西.
所以我会说线在这里是有效的,因为它只在循环中得到赋值,而在其他地方.
任何人都可以对此做出更多的解释,并解释为什么在这里需要它,为什么编译无法解决它?
解决方法
So I would say
line
is effectively final here,as it only gets the assignment in the loop and nowhere else.
不,这不是最终的,因为在变量的生命周期中,它在每个循环迭代中被赋予一个新的值.这是完全相反的决赛.
I get: ‘Local variables referenced from a lambda expression must be final or effectively final’. It seems rather awkward to me.
考虑一下:你将lambda传递给runLater(…).当lambda最终执行时,应该使用哪个行的值?当lambda被创建时,它所具有的值,还是当lambda执行时的值?
规则是lambdas(似乎)在lambda执行时使用当前值.他们不会(似乎)创建一个变量的副本.现在,这个规则在实践中如何实现?
>如果line是一个静态字段,那么很容易,因为没有可以捕获lambda的状态.只要需要,lambda就可以读取该字段的当前值,就像其他代码一样.
>如果line是一个实例字段,那也很容易. lambda可以在每个lambda对象的私有隐藏字段中捕获对对象的引用,并通过该对象访问行字段.
>如果line是一个方法中的局部变量(在你的例子中),这突然不容易.在实现级别,lambda表达式is in a completely different method,并且外部代码没有简单的方法可以共享对只存在于该方法中的变量的访问.
为了能够访问局部变量,编译器必须将变量复制到一些隐藏的,可变的持有者对象(例如1元素数组)中,以便可以从包含方法和lambda两者引用持有者对象,给出他们都可以访问变量.
尽管这个解决方案在技术上是有效的,但是由于一系列的原因,它实现的行为是不希望的.分配持有者对象将给局部变量一个不自然的性能特征,从阅读代码看不明显. (仅定义使用局部变量的lambda将使整个方法的变量变得更慢).更糟的是,它会将微妙的竞争条件引入到其他简单的代码中,这取决于执行lambda的时间.在你的例子中,在lambda执行时,可能会发生任何数量的循环迭代,或者该方法可能已经返回,因此该行变量可以具有任何值或没有定义的值,并且几乎肯定不会有值通缉.所以在实践中你仍然需要单独的,不变的lineReference变量!唯一的区别是编译器不需要你这样做,所以它可以让你编写破碎的代码.由于lambda可以最终在不同的线程上执行,这也将为局部变量引入微妙的并发和线程可见性复杂性,这将需要语言来允许局部变量上的volatile修饰符和其他麻烦.
所以,对于lambda来看,当前变化的局部变量的值会引起很多的大惊小怪(而且如果你需要的话也不会有you can do the mutable holder trick manually的优势).相反,该语言对整个kerfuffle表示不要仅仅要求变量是最终(或有效地最终).这样,lambda可以在lambda创建时捕获局部变量的值,并且不需要担心检测到变化,因为它知道不能有任何变化.
This is something the compiler could also figure out by itself
它确实知道了,这就是为什么它不允许它. lineReference变量对编译器绝对没有任何好处,可以轻松地捕获每个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中引用的任何局部变量(有效地)为final,那么这个混乱就会消失.
在你的代码中,lineReference是最终的.它的值在其生命周期内被分配一次,在每个循环迭代结束之前超出范围,这就是为什么可以在lambda中使用它.
通过在循环体中声明行,可以有一个可选择的循环:
for (;;) { String line = bufferedReader.readLine(); if (line == null) break; runLater(() -> consumeString(line)); }
这是允许的,因为现在行在每个循环迭代结束时超出范围.每次迭代都有一个新的变量,分配一次. (但是,在较低的级别,变量仍然存储在同一个cpu寄存器中,所以不一定要重复地“创建”和“被破坏”,我的意思是,没有额外的费用来声明变量这样的循环,所以没关系.)
注意:所有这一切并不是羊羔的独特之处.它也适用于在方法中以词法方式声明的任何类,其中lambdas从中继承规则.
注2:可以认为,如果他们遵循总是捕获在lambda创建时使用的变量的值的规则,那么lambdas会更简单.那么领域和当地人之间的行为就没有区别,也不需要“最终的或有效的最终”规则,因为在las创建时间之后,lambdas看不到变化是很明确的.但这个规则会有自己的丑恶.作为一个示例,对于在lambda中访问的实例字段x,读取x(捕获x的最终值)和this.x(捕获其最终值,看到其字段x改变)的行为之间将存在差异.语言设计很难.