对 Java 中的 lambda 捕获感到好奇

Curious about lambda capture in Java

我阅读了 https://www.baeldung.com/java-lambda-effectively-final-local-variables 和许多文章 (Whosebug) 但是,还有许多未解决的问题。

  1. 我不知道你为什么要在 lambda 中捕获(复制值)。 以下代码是我附上的 link 中的部分代码。
Supplier<Integer> incrementer(int start) {
  return () -> start++;
}
// start is a local variable, and we are trying to modify it inside of a lambda expression.

他们说

Well, notice that we are returning the lambda from our method. Thus, the lambda won't get run until after the start method parameter gets garbage collected. Java has to make a copy of start in order for this lambda to live outside of this method.

start 变量的生命周期是 incrementer(). 它们都存在于同一个栈上,并且有一个生命周期。但我不明白为什么它说 GC 而没有 运行.

  1. 为什么只有局部变量必须final或有效final?他们(baeldung)说它是因为 Concurrency Issues.

由于堆栈是为每个线程分配的,所以不会有并发问题。相反,当静态成员变量会导致并发问题时,为什么局部变量需要是最终的?

They both exist on the same stack and have a lifecycle together.

不,他们没有。

这里:

public class OhDearThatWasALieWasntIt {
   void haha() throws Exception {
     var supplier = incrementer(20);
     Thread t = new Thread() {
       public void run() {
         supplier.get();
       }
     }
   }
}

给你。他们根本不共享堆栈。事实上,您的 incrementer 本地变量需要从一个线程一直传输到另一个完全不同的线程。

一个简单的事实是,编译器不知道 lambda 将在哪里结束以及谁应该 运行 它。

Since the stack is allocated for each thread, there can be no concurrency issues.

Baeldung 可能过于简单化了。如果在 lambda 中使用的局部变量是 not final,那么只有 2 个选项:

[A] lambda 获得了一个克隆,这非常令人困惑。

[B] 变量被提升到堆中,我们现在允许 volatile 在本地变量上;局部变量不可能与其他线程共享的格言被搁置一旁,并发问题比比皆是。

让我们看看实际效果:

void meanCode() {
  int local = 100;
  Runnable r = () -> {
    for (int i = 0; i < 10; i++) { 
      System.out.println(local++);
    }
  };

  Thread a = new Thread(r);
  a.start();
  Thread.sleep(5);
  for (int i = 0; i < 10; i++) { 
    System.out.println(local++);
  }
}

或者 local 现在是在 2 个地方使用的变量,因此上面的代码是竞争条件,或者,分发了一个克隆,并且 Runnable 和 for 循环都在上面的片段得到了他们自己的 local 的本地副本,因此没有竞争条件,按顺序打印 100109,但两者都打印 运行s 任意交错(我想有还剩下一点竞争条件)。您秘密拥有 2 个变量的事实令人难以置信。

鉴于这两个选项完全令人困惑,java 相反根本不允许。使用(有效的)final 变量,java 只需将副本提供给 lambda,从而巧妙地回避任何并发问题。它也不会混淆,因为变量(实际上)是最终的。

但是这里没有主题!

是的,你知道的。编译器怎么可能知道呢?编译器(和 运行time)一次只处理一个 类。编译器不会 'treeshake' 你的整个项目来煞费苦心地确保你的代码永远不会在这些东西在多个线程中结束的情况下结束。即使它以某种方式做到了,也许以后有人会重新编译这个代码库的一半,或者只是添加一些现在可以做的 类。

变量的捕获与并发执行和安全完全没有关系,原因完全不同。

在回答你的问题之前,让我先解释一下什么是lambda表达式。

什么是 lambda 表达式

当您使用 lambda 表达式时,在编译和运行期间会发生一些对开发人员隐藏的事情。 lambda 表达式是 java 语言的一部分也毫无价值,它不存在于生成的字节码中。

我将使用以下代码作为示例

public class GreeterFactory {

    private String header = "Hello ";
    
    public Function<String, String> createGreeter(int greeterId){
        Function<String, String> greeter = username -> {
            return String.format("(%s) %s: %s", greeterId, header, username);
        };
        
        return greeter;
    }
}

lamba表达式编译成匿名方法

当 javac 将 java 编译成字节码时,它会将你的 lambda 主体转换为嵌入 class 中的新方法(这就是为什么 lambda 表达式可以被认为是匿名的方法)。

这是字节码中的内容(使用 javap 工具反编译):

Compiled from "GreeterFactory.java"
public class various.GreeterFactory {
  private java.lang.String header;
  public various.GreeterFactory();
  public java.util.function.Function<java.lang.String, java.lang.String> createGreeter(int);
  private java.lang.String lambda$createGreeter[=11=](int, java.lang.String);
}

如您所见,GreeterFactory class 不仅有我编写的 createGreeter 方法。它现在还将具有由编译器生成的 lambda$createGreeter[=14=] 方法。

您在这里可能注意到的一件事是生成的方法有两个参数(int 和 String),即使在我的 lambda 中我只声明了一个参数 - String。这样做的原因是因为在运行时,不仅会使用我传递的参数(当我执行 apply 方法形式 Function 接口时)调用此方法,还会调用所有“捕获”的值。这让我们指出第 2 点:

运行时的 Lambda 表达式

我们已经知道 lambda 被转换为实际的方法,现在的问题是:我从执行该 lamda 表达式中得到的结果到底是什么(除了它是实现 Function 接口的事实之外)?

Function<String, String> greeter 变量实际上指向一个内部对象:

  • 引用了 this GreeterFactory 对象(以便稍后可以在其上调用方法)
  • 包含所有(在 lambda 表达式的主体中使用)“捕获”的局部变量(在我的示例中:greeterId 的值)
  • 引用了生成的lambda$createGreeter[=14=]方法

您可以在调试器中检查该对象时看到它。您将看到以下内容:

请注意,greeter 对象恰好具有我提到的那两个值(参考 this GreeterFactory 对象和从 [=21] 复制的值 23 =]). 这正是 lambda 表达式中“捕获”的含义。

稍后在该对象上执行 apply 时,它实际上会调用 this GreeterFactory 对象上的 lambda$createGreeter[=14=] 方法,其中包含您捕获的所有值 + 参数传入 apply 方法。

返回问题

我希望我已经在上面解释了什么是“捕获”以及它是如何工作的。 让我们进入final/effectively决赛。

为什么捕获的变量必须是有效的最终变量。

免责声明:我没有找到任何关于它的官方信息,这只是我的假设,因此:我可能是错的。

请注意,lambda 仅存在于 java 语言级别,而不存在于字节码。 在解释了 lambda 的工作原理(新方法的生成)之后,我认为在技术上也可以捕获非有效最终变量。

我认为 lambda 表达式的设计者选择这种方式的原因是更专注于帮助开发人员编写无错误的代码。

如果捕获的变量不是有效最终的,这意味着:它们可以在 lambda 之外和 lambda 内进一步修改,从开发人员的角度来看,这可能会导致许多混淆和误解,从而有效地导致许多错误. IE。开发人员可能期望在 lambda 中更改变量的值应该影响外部方法范围内的这个变量(这是因为它在 lambda 体内的语言中不可见,我们实际上在新生成的方法的范围内),或者他们可能期望相反。简而言之:一片混乱。

我认为这就是做出这种决定的原因,也是编译器和语言强制执行它的原因,即将 lambda 的范围和嵌入方法的范围视为一个范围(即使在运行时它们是不同的范围)。

请注意,以前对于匿名 classes 捕获的变量也是如此,因此开发人员已经熟悉这种方法。

为什么lambda可以随意修改对象中的字段?因为它只是此对象 class 中的一个方法,并且与任何其他方法一样,它可以自由访问其所有成员。期望不同的行为会令人困惑。