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
The relevant aspect is in the output of javap
but you didn't include it in your question. At the very end of the javap output: (make sure to run with javap -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
This... doesn't really come across as useful, but it is the underlying mechanism that makes all this work.
The relevant call is the invokedynamic
bytecode instruction. invokedynamic
is, well, dynamic: The first time invokedynamic
is encountered the JVM will take a slow path and executes some code that can arbitrarily decide what this invocation is actually going to end up looking like. Any further executions of that same invokedynamic
call no longer do that - they 'memoize'. So, an invokedynamic
call can result in any kind of actual method invocation you want (here, it'll end up being a partial application of that lambda$makeAdder$0(float, float)
, of course), and is nevertheless fast.
javap
's output isn't all that enlightening. A lot of the crucial moving parts of it all are actually in the java.lang.invoke.LambdaMetaFactory
class which is a real class in the JVM whose source is part of the core JDK and thus open.
This baeldung tutorial on invokedynamic
is probably something you want to go through top to bottom if you are interested in fully understanding how this works.