Sobes.tech
Middle+

What makes lambda expressions work and what happens 'under the hood'?

sobes.tech AI

Answer from AI

Lambda expressions in Java work through the following mechanisms:

  1. Functional interfaces: A lambda expression must be assigned to a variable, returned from a method, or passed to a method whose parameter type is a functional interface. A functional interface is an interface with exactly one abstract method (SAM - Single Abstract Method). The @FunctionalInterface annotation is optional but recommended for compiler checks.

  2. InvokeDynamic (invokeDynamic): Instead of generating an anonymous inner class in bytecode at compile time, as was done for older versions of anonymous classes, Java uses the invokeDynamic instruction. This instruction was added in Java 7 to support dynamically typed languages on the JVM, and in Java 8 it is used to implement lambda expressions and method references. invokeDynamic defers method call resolution until runtime.

What happens 'under the hood':

At compile time:

  • The compiler analyzes the lambda expression.
  • It determines the type of the functional interface to which the lambda is assigned.
  • A special synthetic method (often with the prefix lambda$) is generated in the same class containing the lambda logic. This method has the same signature as the abstract method of the functional interface.
  • At the lambda expression usage site, the compiler generates an invokeDynamic instruction. This instruction contains a reference to the bootstrap method (LambdaMetafactory.metafactory).

At runtime:

  • When the JVM encounters the invokeDynamic instruction, it first calls the bootstrap method (LambdaMetafactory.metafactory).
  • The bootstrap method dynamically generates a call site in memory.
  • The call site creates and returns an instance of a class that implements the functional interface. This instance can be:
    • Created on the fly (e.g., using a generated Constant CallSite).
    • Cached for subsequent use (for stateless lambdas).
    • Represented as a reference to an existing instance of the functional interface (e.g., for stateless, Serializable-implementing lambdas).
  • This generated class delegates the call of the abstract method of the functional interface to the synthetic method (lambda$) generated by the compiler.
  • After the first call to the bootstrap method, the call site remembers which object to return, and subsequent invokeDynamic calls for the same lambda can quickly retrieve this object or call it directly without invoking the bootstrap method again.

Thus, lambdas do not always create a new class for each instantiation. For lambdas that do not capture variables from the external context (stateless lambdas), the implementation of the functional interface can be a singleton or an instance that is cached and reused. Lambdas that capture variables (stateful lambdas) require creating a new instance of a class that will store the captured variables.

Using invokeDynamic allows the JIT compiler to optimize method calls of functional interfaces more efficiently than the traditional anonymous class model, as the JIT can make more informed decisions at runtime.

// Example of a functional interface
@FunctionalInterface
interface MyFunction {
    String apply(String input);
}

public class LambdaExample {
    public static void main(String[] args) {
        // Using a lambda expression
        MyFunction func = s -> "Processed: " + s;

        String result = func.apply("Test");
        System.out.println(result);
    }
}
What makes lambda expressions work and what happens… - sobes.tech