Lambdas are closures, which means they capture their surrounding state, but they don’t actually capture anything in the traditional sense.
Let’s watch a simple lambda in action.
import java.util.function.Function;
public class LambdaExample {
public static void main(String[] args) {
int offset = 10;
Function<Integer, Integer> addOffset = x -> x + offset;
System.out.println(addOffset.apply(5)); // Output: 15
}
}
When you compile this, the offset variable isn’t copied into the lambda. Instead, the offset variable is accessed directly. This is where invokeDynamic comes in.
Before invokeDynamic, Java had a few ways to handle dynamic method invocation, but they were often clunky or inefficient for the kind of dynamic behavior lambdas require. invokeDynamic was introduced in Java 7 specifically to make these kinds of dynamic operations, like method calls on dynamically created objects or method references, more efficient and flexible. It allows the JVM to defer the binding of a method call until runtime, delegating the actual lookup and invocation to a "bootstrap method."
Here’s how it works for our lambda:
-
Compilation: When the Java compiler sees
x -> x + offset, it doesn’t generate code to copyoffsetinto a new object. Instead, it generates aninvokedynamicbytecode instruction. This instruction points to a specific "bootstrap method" and a "name and descriptor" that describe the method to be invoked. -
Bootstrap Method: The bootstrap method is the key. It’s a method that runs once when the
invokedynamicinstruction is first encountered. Its job is to:- Determine the actual method to be called.
- Generate a specialized, efficient method handle for that call.
- Link this method handle to the
invokedynamicinstruction.
For lambdas, the JVM’s built-in bootstrap method (usually
java.lang.invoke.LambdaMetafactory.metaFactory) analyzes the lambda expression. It figures out:- The type of the functional interface (
Function<Integer, Integer>). - The target method of the lambda (the
applymethod ofFunction). - The captured variables (
offset).
-
Method Handle Generation: The
LambdaMetafactorythen generates a new class on the fly (or uses an existing one if possible). This class will contain a method that implements the lambda’s logic. Crucially, this generated method will have direct access to theoffsetvariable from the enclosing scope. It doesn’t need to copy it. ThemetaFactoryreturns aCallSiteobject that holds aMethodHandlepointing to this generated implementation. -
Invocation: The
invokedynamicinstruction now has aMethodHandlethat directly invokes the lambda’s implementation. The first timeaddOffset.apply(5)is called, theinvokedynamicinstruction uses theCallSiteto find theMethodHandle. Subsequent calls use the cachedMethodHandledirectly, making it as fast as a regular method call.
The magic of invokeDynamic for lambdas is that it allows the JVM to create efficient, specialized code at runtime without the overhead of traditional reflection or creating explicit anonymous inner classes for every lambda instance. The LambdaMetafactory is incredibly smart; it can generate code that directly accesses captured variables, making lambdas performant and behave like true closures.
What most people don’t realize is that the LambdaMetafactory generates an entire class dynamically for your lambda. This class is optimized for the specific context, including the captured variables and the target method. It’s not just a simple method pointer; it’s a fully realized implementation that the JVM can execute with high efficiency.
The next step is understanding how method references (::) leverage invokeDynamic in a very similar, yet distinct, way.