I'm trying to figure out exactly how lambdas and closures work in the JVM. To that end, I've tried compiling this simple test case:
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));
}
}
Disassembling the resulting byte code is interesting:
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
Some of the above is clear, some less so. The part I'm most puzzled about right now is the implementation of the lambda function, lambda$makeAdder$0(java.lang.Float, java.lang.Float)
. That signature suggests the lambda has two parameters even though it was declared with just one.
Well, it's obvious what the extra one is for; it's for the value of a
that was bound into the closure. So at one level, that answers the question of how Java closures are supplied with the values of bound variables: they are prepended to the parameter list.
But then, how does the ultimate caller know about this? The disassembled code for main
looks isomorphic to the source code, i.e. completely innocent of knowledge about how closures are implemented. It seems to be supplying one argument to makeAdder
, then the second argument to the lambda. In other words, supplying just one argument to the lambda.
How is the first argument also supplied to the lambda?
Does it anything to do with the final BootstrapMethods
section of the disassembled code?
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