为什么在 lambda 表达式中使用的变量应该是 final 或有效的 final

Why variable used in lambda expression should be final or effectively final

这个问题之前已经被问过
我的问题为什么在 上得到了回答
但我对答案有些怀疑。 提供的答案提到-

Although other answers prove the requirement, they don't explain why the requirement exists.

The JLS mentions why in §15.27.2:

The restriction to effectively final variables prohibits access to dynamically-changing local variables, whose capture would likely introduce concurrency problems.

To lower the risk of bugs, they decided to ensure captured variables are never mutated. I am confused by the statement that it would lead to concurrency problems.

我在 Baeldung 上阅读了有关并发问题的文章,但我仍然对它如何导致并发问题感到有点困惑,谁能帮我举个例子。 提前致谢。

出于同样的原因,匿名 classes 要求它们从自身作用域中出来时使用的变量必须是只读的 -> final.

final int finalInt = 0;
int effectivelyFinalInt = 0;
int brokenInt = 0;
brokenInt = 0;

Supplier<Integer> supplier = new Supplier<Integer>() {
    @Override
    public Integer get() {
        return finalInt;                        // compiles
        return effectivelyFinalInt;             // compiles
        return brokenInt;                       // doesn't compile
    }
};

Lambda 表达式只是仅使用一个抽象方法 (@FunctionalInterface) 实现接口的实例的快捷方式。

Supplier<Integer> supplier = () -> brokenInt;   // compiles
Supplier<Integer> supplier = () -> brokenInt;   // compiles
Supplier<Integer> supplier = () -> brokenInt;   // doesn't compile

我很难阅读 Java Language specification 以支持我的以下陈述,但是它们是合乎逻辑的:

  • 请注意,对 lambda 表达式的求值会生成功能接口的实例。

  • 请注意,实例化接口需要实现其所有抽象方法。作为表达式执行会产生匿名 class.

  • 请注意,匿名 class 始终是内部 class。

  • 每个内部 class 只能访问其范围之外的最终或有效最终变量:Accessing Members of an Enclosing Class

    In addition, a local class has access to local variables. However, a local class can only access local variables that are declared final. When a local class accesses a local variable or parameter of the enclosing block, it captures that variable or parameter.

创建 lambda 表达式的实例时,它引用的封闭范围内的所有变量都会复制到其中。现在,假设是否允许修改,现在您正在使用该副本中的陈旧值。另一方面,假设在 lambda 内部修改了副本,但封闭范围内的值仍然没有更新,留下不一致。因此,为了防止这种情况的发生,语言设计者施加了这种限制。这可能也会让他们的生活更轻松。可以找到匿名内部 class 的相关答案 here.

另一点是,您将能够传递 lambda 表达式,如果它被转义并且另一个线程执行它,而当前线程正在更新同一个局部变量,那么也会出现一些并发问题。

我想先说明一下我在下面展示的并不是 lambda 的实际实现方式。实际实现涉及 java.lang.invoke.LambdaMetafactory 如果我没记错的话。我的回答利用了一些不准确的地方来更好地证明这一点.


假设您有以下条件:

public static void main(String[] args) {
  String foo = "Hello, World!";
  Runnable r = () -> System.out.println(foo);
  r.run();
}

请记住,lambda 表达式 shorthand 用于声明功能接口的实现。 lambda 主体是所述功能接口的单个​​抽象方法的实现。在 运行 时间创建了一个实际对象。所以上面的结果是一个 class 实现 Runnable.

的对象

现在,上面的 lambda 主体从封闭方法中引用了一个局部变量。作为 lambda 表达式的结果创建的实例“捕获”该局部变量的值。几乎(但不是真的)就像你有以下内容:

public static void main(String[] args) {
  String foo = "Hello, World!";

  final class GeneratedClass implements Runnable {
    
    private final String generatedField;

    private GeneratedClass(String generatedParam) {
      generatedField = generatedParam;
    }

    @Override
    public void run() {
      System.out.println(generatedField);
    }
  }

  Runnable r = new GeneratedClass(foo);
  r.run();
}

现在应该更容易在此处看到支持并发的问题:

  1. 局部变量不被视为“共享变量”。这在 §17.4.1 of the Java Language Specification:

    中说明

    Memory that can be shared between threads is called shared memory or heap memory.

    All instance fields, static fields, and array elements are stored in heap memory. In this chapter, we use the term variable to refer to both fields and array elements.

    Local variables (§14.4), formal method parameters (§8.4.1), and exception handler parameters (§14.20) are never shared between threads and are unaffected by the memory model.

    也就是说,局部变量不在Java的并发规则范围内,不能在线程间共享。

  2. 在源代码级别,您只能访问局部变量。您看不到生成的字段。

我想 Java 可以设计成在 lambda 体内修改局部变量只写入生成的字段,而在 lambda 体外修改局部变量只写入局部变量。但正如您可能想象的那样,这会让人感到困惑和违反直觉。根据源代码,您将有两个变量似乎是一个变量。更糟糕的是,这两个变量的值可能会有所不同。

另一种选择是没有生成字段。但请考虑以下几点:

public static void main(String[] args) {
  String foo = "Hello, World!";
  Runnable r = () -> {
    foo = "Goodbye, World!"; // won't compile
    System.out.println(foo);
  }
  new Thread(r).start();
  System.out.println(foo);
}

这里应该发生什么?如果没有生成的字段,则局部变量正在被第二个线程修改。但是局部变量不能在线程之间共享。因此,这种方法是不可能的,至少在不对 Java 和 JVM 进行可能不重要的更改的情况下是不可能的。

因此,据我了解,设计者在这种情况下制定了局部变量必须是 final 或实际上是 final 的规则,以避免并发问题并使开发人员陷入深奥的问题中。