为什么实例字段不需要是最终的或有效的最终的才能在 lambda 表达式中使用?

我正在Java中练习lambda表达式。我知道局部变量需要是最终的或有效的最终的,根据Java SE 16 Lambda Body的Oracle文档:

在 lambda 表达式中使用但未声明的任何局部变量、形式参数或异常参数必须是最终的或实际上是最终的 (§4.12.4),如 §6.5.6.1 中所指定。

它没有说明原因。搜索我发现了类似的问题,为什么lambda中的变量必须是最终的还是有效的最终的?,其中StackOverflow用户“snr”用下一个引号回应:

到目前为止,Java 中的局部变量一直不受争用条件和可见性问题的影响,因为只有执行声明它们的方法的线程才能访问它们。但是,lambda 可以从创建它的线程传递到另一个线程,因此,如果由第二个线程计算的 lambda 能够变异局部变量,则该免疫力将会丧失。

这就是我的理解:一个方法一次只能由一个线程(假设thread_1)执行。这可确保该特定方法的局部变量仅由thread_1修改。另一方面,lambda可以传递到不同的线程(thread_2),因此...如果thread_1以 lambda 表达式结束并继续执行方法的其余部分,则可以更改局部变量的值,同时,thread_2可能会更改 lambda 表达式中的相同变量。然后,这就是存在此限制的原因(局部变量必须是最终的或有效的最终变量)。

很抱歉冗长的解释。我说得对吗?

但接下来的问题是:

  • 为什么这种情况不适用于实例变量?
  • 如果thread_1在thread_2的同时更改实例变量(即使它们不执行 lambda 表达式),会发生什么情况?
  • 实例变量是否以其他方式受到保护?

我对Java没有太多经验。抱歉,如果我的问题有明显的答案。


答案 1

这个问题与线程安全无关,真的。对于为什么总是可以捕获实例变量,有一个简单明了的答案:总是有效的最终结果。也就是说,在创建访问实例变量的 lambda 时,始终有一个已知的固定对象。请记住,名为 的实例变量始终有效地等效于 。thisfoothis.foo

所以

class MyClass {
  private int foo;
  public void doThingWithLambda() {
    doThing(() -> { System.out.println(foo); })
  }
}

可以将 lambda 重写为,因此等效于doThing(() -> System.out.println(this.foo); })

class MyClass {
  private int foo;
  public void doThingWithLambda() {
    final MyClass me = this;
    doThing(() -> { System.out.println(me.foo); })
  }
}

...except 已经是 final,不需要复制到另一个局部变量(尽管 lambda 会捕获引用)。this

当然,所有正常的线程安全警告都适用。如果您的 lambda 被传递给多个线程并修改变量,那么如果不使用 lambda,则不会发生完全相同的情况,并且除了变量的线程安全性(例如,如果它们是易失性的)之外,没有额外的线程安全适用,或者如果您的 lambda 使用其他机制来安全访问变量。Lambdas对线程安全没有任何特殊作用,它们也不会对实例变量做任何特殊的事情。它们只捕获对实例变量的引用,而不是对实例变量的引用。this


答案 2

其他答案已经提供了很好的上下文,说明为什么这是Java中的一个限制。我想提供一些关于其他语言如何处理这个问题的背景知识,当它们不强制要求局部变量被认为是不可变的(即)。final

建议的要点是,“堆”值(即字段)本质上可以从其他线程访问,而“堆栈”值(即局部变量)本质上只能从声明值的方法中访问。这是事实。因此,由于字段存储在堆上,因此可以在方法完成后对其进行更改。相反,一旦方法完成,堆栈值就会消失。

Java 选择遵循这些语义,因此在方法完成后,绝不能修改局部变量。这是一个公平的设计决策。但是,某些语言确实选择在方法退出后允许局部变量突变。那怎么可能呢?

在 C#(我最熟悉的语言,但其他语言(如 JavaScript)也允许这些构造)中,当您在 lambda 中引用局部变量时,编译器会检测到这一点,并在幕后生成一个全新的类来存储局部变量。因此,编译器不会在堆栈上声明变量,而是检测到它已在 lambda 内部引用,因此而是实例化该类以存储值。因此,此(在后台)行为将堆栈值转换为堆值。(您实际上可以反编译此类代码并查看这些编译器生成的类)

这个决定并非没有代价。显然,实例化一个类只是为了容纳一个整数,这显然更昂贵。在Java中,您可以保证这永远不会发生。在 C# 等语言中,需要仔细推理才能知道变量是否已“提升”到该生成的类中。

因此,最终基本原理成为设计决策之一。在Java中,你不能向自己开枪。在C#中,他们认为大多数时候性能后果并不是什么大问题。

也就是说,C#的决定经常是混乱和错误的根源,特别是在循环中的循环迭代器变量周围(循环变量可以(并且必须)突变)并传递给lambda,如Eric Lippert的博客文章中所述。这非常成问题,以至于他们决定为变体的编译器引入(罕见的)重大更改。foriforeach

另一方面,我很享受在C#中的lamda内部变异局部变量的自由。但这两个决定都不是没有代价的。

这个答案绝对不是试图倡导任何一个决定,但我认为有必要详细说明其中一些设计选择。


推荐