编译的 lambda 函数中的额外参数从何而来?

Where does the extra parameter in a compiled lambda function come from?

提问人:rwallace 提问时间:1/23/2023 更新时间:1/23/2023 访问量:85

问:

我试图弄清楚 lambda 和闭包在 JVM 中是如何工作的。为此,我尝试编译了这个简单的测试用例:

import java.util.function.*;

class Adder {
  static Function<Float, Float> makeAdder(Float a) {
    return b -> a + b;
  }

  public static void main(String[] args) {
    Function<Float, Float> f = makeAdder(1.23f);
    System.out.println(f.apply(4.56f));
  }
}

反汇编生成的字节码很有趣:

  static java.util.function.Function<java.lang.Float, java.lang.Float> makeAdder(java.lang.Float);
    descriptor: (Ljava/lang/Float;)Ljava/util/function/Function;
    flags: (0x0008) ACC_STATIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokedynamic #7,  0              // InvokeDynamic #0:apply:(Ljava/lang/Float;)Ljava/util/function/Function;
         6: areturn
      LineNumberTable:
        line 4: 0
    Signature: #48                          // (Ljava/lang/Float;)Ljava/util/function/Function<Ljava/lang/Float;Ljava/lang/Float;>;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=2, args_size=1
         0: ldc           #11                 // float 1.23f
         2: invokestatic  #12                 // Method java/lang/Float.valueOf:(F)Ljava/lang/Float;
         5: invokestatic  #18                 // Method makeAdder:(Ljava/lang/Float;)Ljava/util/function/Function;
         8: astore_1
         9: getstatic     #23                 // Field java/lang/System.out:Ljava/io/PrintStream;
        12: aload_1
        13: ldc           #29                 // float 4.56f
        15: invokestatic  #12                 // Method java/lang/Float.valueOf:(F)Ljava/lang/Float;
        18: invokeinterface #30,  2           // InterfaceMethod java/util/function/Function.apply:(Ljava/lang/Object;)Ljava/lang/Object;
        23: invokevirtual #35                 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
        26: return
      LineNumberTable:
        line 9: 0
        line 10: 9
        line 11: 26

  private static java.lang.Float lambda$makeAdder$0(java.lang.Float, java.lang.Float);
    descriptor: (Ljava/lang/Float;Ljava/lang/Float;)Ljava/lang/Float;
    flags: (0x100a) ACC_PRIVATE, ACC_STATIC, ACC_SYNTHETIC
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: invokevirtual #41                 // Method java/lang/Float.floatValue:()F
         4: aload_1
         5: invokevirtual #41                 // Method java/lang/Float.floatValue:()F
         8: fadd
         9: invokestatic  #12                 // Method java/lang/Float.valueOf:(F)Ljava/lang/Float;
        12: areturn
      LineNumberTable:
        line 4: 0
        

以上有些是清楚的,有些则不那么清楚。我现在最困惑的部分是 lambda 函数的实现。该签名表明 lambda 有两个参数,即使它只用一个参数声明。lambda$makeAdder$0(java.lang.Float, java.lang.Float)

嗯,很明显,额外的一个是做什么用的;这是为了绑定到闭包中的价值。因此,在某种程度上,这回答了如何为 Java 闭包提供绑定变量值的问题:它们被附加到参数列表之前。a

但是,最终的来电者是如何知道这一点的呢?反汇编的代码看起来与源代码同构,即完全不知道闭包是如何实现的。它似乎向 提供了一个参数,然后向 lambda 提供了第二个参数。换句话说,只向 lambda 提供一个参数。mainmakeAdder

第一个参数是如何提供给 lambda 的?

它与反汇编代码的最后一部分有什么关系吗?BootstrapMethods

BootstrapMethods:
  0: #56 REF_invokeStatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
    Method arguments:
      #63 (Ljava/lang/Object;)Ljava/lang/Object;
      #64 REF_invokeStatic Adder.lambda$makeAdder$0:(Ljava/lang/Float;Ljava/lang/Float;)Ljava/lang/Float;
      #67 (Ljava/lang/Float;)Ljava/lang/Float;
InnerClasses:
  public static final #74= #70 of #72;    // Lookup=class java/lang/invoke/MethodHandles$Lookup of class java/lang/invoke/MethodHandles
Java Lambda JVM 闭包 invokedynamic

评论

0赞 Elliott Frisch 1/23/2023
在代码中,有两个值(构造“函子”时)和(应用它时)。这有帮助吗?你是在问它是如何得到的还是?1.23f4.56f1.234.56
0赞 rwallace 1/23/2023
@ElliottFrisch 我问的是 1.23 是如何作为参数提供给函子的,因为在 的源代码和字节码中,它似乎只是作为参数提供给 。mainmakeAdder
1赞 Elliott Frisch 1/23/2023
请参阅 JEP 126:Lambda 表达式和虚拟扩展方法Project Lambda,如果您真的想深入了解 how(提示:)。1: invokedynamic #7, 0

答:

2赞 rzwitserloot 1/23/2023 #1

相关方面在输出中,但您没有将其包含在您的问题中。在 javap 输出的最后:(确保使用 !javapjavap -c -v

SourceFile: "Test.java"
BootstrapMethods:
  0: #56 REF_invokeStatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
    Method arguments:
      #63 (Ljava/lang/Object;)Ljava/lang/Object;
      #64 REF_invokeStatic Adder.lambda$makeAdder$0:(Ljava/lang/Float;Ljava/lang/Float;)Ljava/lang/Float;
      #67 (Ljava/lang/Float;)Ljava/lang/Float;
InnerClasses:
  public static final #74= #70 of #72;    // Lookup=class java/lang/invoke/MethodHandles$Lookup of class java/lang/invoke/MethodHandles

这。。。并没有真正有用,但它使所有这些工作的基础机制。

相关的调用是字节码指令。 是动态的:第一次遇到JVM会走一条慢路,并执行一些代码,这些代码可以任意决定这个调用的实际结果。同一调用的任何进一步执行都不再这样做 - 它们会“记住”。因此,调用可以导致您想要的任何类型的实际方法调用(当然,它最终会成为该方法的部分应用),并且速度很快。invokedynamicinvokedynamicinvokedynamicinvokedynamicinvokedynamiclambda$makeAdder$0(float, float)

javap的输出并不是那么有启发性。它的许多关键移动部分实际上都在类中,这是 JVM 中一个真正的类,其源代码是核心 JDK 的一部分,因此是开放的。java.lang.invoke.LambdaMetaFactory

如果你有兴趣完全理解它是如何工作的,这个关于invokedynamic的baeldung教程可能是你想从上到下浏览的内容。