Java中的Lambda是如何实现的

2023-03-15 13:55:39 浏览数 (1)

首先来看段Java代码

代码语言:javascript复制
import java.util.function.Consumer;
public class Test {
  public static void main(String[] args) {
    String s1 = "hello";
    Consumer<String> c =
        s2 -> {
          System.out.println(s1);
          System.out.println(s2);
        };
    c.accept("world");
  }
}

看下其对应的字节码

代码语言:javascript复制
➜  javac Test.java                                  
➜  javap -p -v Test                                 
...
{
  ...
  public static void main(java.lang.String[]);
    ...
         0: ldc           #2                  // String hello
         2: astore_1
         3: aload_1
   4: invokedynamic #3,  0              // InvokeDynamic #0:accept:(Ljava/lang/String;)Ljava/util/function/Consumer;
         9: astore_2
        10: aload_2
        11: ldc           #4                  // String world
  13: invokeinterface #5,  2            // InterfaceMethod java/util/function/Consumer.accept:(Ljava/lang/Object;)V
        18: return
    ...


  private static void lambdamain0(java.lang.String, java.lang.String);
    ...
         0: getstatic     #6                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: aload_0
    4: invokevirtual #7                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         7: getstatic     #6                  // Field java/lang/System.out:Ljava/io/PrintStream;
        10: aload_1
    11: invokevirtual #7                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        14: return
    ...
}
...

在上面的字节码中,我们可以看到一个名为 lambdamain0 的方法,该方法是在编译阶段自动生成的,其对应于示例源码中的lambda方法体。

在main方法的字节码中,invokedynamic是整个lambda实现的关键,不过由于该字节码在JVM中的实现逻辑非常复杂,在这里我们就不看具体代码了,只说下大致思路。

该字节码的最终目的是为了创建一个对象,且该对象要实现java.util.function.Consumer接口,这样这个对象才可以赋值给上面示例源码中的 Consumer<String> c 变量。

那这个对象对应的类是哪里来的呢?对,也是动态生成的。

JVM在执行invokedynamic字节码时,会根据class文件中提供的各种信息,调用java.lang.invoke.LambdaMetafactory.metafactory方法来动态生成这个类。

该类的生成结果我们可以通过下面的方式看下

➜ java -Djdk.internal.lambda.dumpProxyClasses Test hello world

执行完上面的命令后,会在当前目录生成一个名为 Test$Lambda1.class 的文件,该文件的内容就是invokedynamic字节码动态生成的类,我们反编译看下

代码语言:javascript复制
import java.lang.invoke.LambdaForm.Hidden;
import java.util.function.Consumer;


// $FF: synthetic class
final class Test$Lambda1 implements Consumer {
  private final String arg$1;


  private Test$Lambda1(String var1) {
    this.arg$1 = var1;
  }


  private static Consumer get$Lambda(String var0) {
    return new Test$Lambda1(var0);
  }


  @Hidden
  public void accept(Object var1) {
    Test.lambdamain0(this.arg
  }
}

恩,正如我们所想,该类确实是实现了java.util.function.Consumer接口。

在该类生成完毕之后,invokedynamic字节码会调用其getLambda方法来创建一个TestLambda1实例,该实例创建成功,也意味着invokedynamic字节码执行完毕。

该实例接着被赋值给了Consumer<String> c 变量,之后调用其accept方法,而在accept方法中又调用了编译阶段生成的Test.lambda

之后的流程就是一般的Java执行流程了,在此不做过多介绍。

依据上面的示例,我们再来总结下lambda是如何实现的:

在编译阶段,javac会自动生成一个lambdamain0方法,该方法对应了lambda的方法体。

在运行阶段,当执行invokedynamic字节码时,JVM会根据编译阶段提供的各种信息,调用java.lang.invoke.LambdaMetafactory.metafactory方法动态生成Test$Lambda1类,该类实现了java.util.function.Consumer接口,且在其accept方法中直接调用了编译阶段生成的lambdamain0方法。

当TestLambda当Test类生成完毕后,invokedynamic字节码会调用其get$Lambda方法,生成一个TestLambda

该实例创建完毕也意味着invokedynamic字节码执行完毕。

该实例创建完毕之后,赋值给了示例源码中的Consumer<String> c变量,然后调用其accept方法,传入world字符串。

在Test$Lambda1类中的accept方法中,其直接调用了编译阶段生成的Test.lambdamain0方法,传入参数变量为arg

Test.lambdamain0方法最终执行了lambda的方法体代码。

至此,整个流程结束。

0 人点赞