Lambdas and invokedynamic: How They Really Work
Why a lambda is not an anonymous class, what invokedynamic and LambdaMetafactory do at first call, the allocation difference between capturing and non-capturing lambdas, and what this means.
On this page
Interviewers ask "what is the difference between a lambda and an anonymous inner class?" expecting "less boilerplate". The real answer involves a bytecode instruction added specifically to support dynamic languages, and it explains a measurable performance difference.
Key Takeaways
- A lambda compiles to an
invokedynamiccall site plus a private static method holding the body. No extra class file. LambdaMetafactoryspins the implementation class at first execution and links the call site permanently.- A non-capturing lambda is a singleton — allocated once, reused forever. A capturing one allocates per evaluation.
thisinside a lambda is the enclosing instance; inside an anonymous class it is the anonymous object.- Captured locals must be effectively final because the value is copied, not the variable.
What the compiler emits
public class Source {
public Runnable make() {
return () -> System.out.println("hello");
}
}The compiler does two things. It moves the body into a synthetic private static method, and it
replaces the lambda expression with an invokedynamic instruction naming a bootstrap method:
public java.lang.Runnable make();
0: invokedynamic #2, 0 // InvokeDynamic #0:run:()Ljava/lang/Runnable;
5: areturn
private static void lambda$make$0(); // the body, lifted out
0: getstatic #3 // Field java/lang/System.out
3: ldc #4 // String hello
5: invokevirtual #5 // println
8: return
BootstrapMethods:
0: #20 java/lang/invoke/LambdaMetafactory.metafactory(...)Compare that with an anonymous class, which produces a genuine second class file — Source$1.class —
present on disk, loaded eagerly, and counted in class-loading statistics. A codebase converting a
thousand anonymous classes to lambdas removes a thousand class files, which measurably improves
startup and reduces Metaspace usage.
What happens at runtime
invokedynamic was added in Java 7 for dynamic languages on the JVM; Java 8 reused it for lambdas.
The benefit is that the strategy for creating the implementation is not baked into the bytecode.
The JDK is free to change it — and has, using hidden classes since Java 15 — without recompiling a
single application.
After linking, the call site is a direct invocation that the JIT inlines like any other. A lambda in a hot loop compiles to the same machine code as the equivalent hand-written call.
Capturing versus non-capturing
This is the part with a measurable cost.
// Non-capturing: depends on nothing outside itself.
Supplier<List<String>> factory = ArrayList::new;
Predicate<String> notEmpty = s -> !s.isEmpty();
// Capturing: closes over `prefix`.
String prefix = "order-";
Predicate<String> matches = s -> s.startsWith(prefix);A non-capturing lambda has no state, so LambdaMetafactory creates one instance and the call site
returns that same object every time. Evaluating the expression in a loop a million times allocates
nothing.
A capturing lambda needs somewhere to put the captured values, so a new object is allocated on each evaluation. In a hot loop that is real garbage:
for (Order order : orders) {
// Captures `order` — a new Predicate object per iteration.
process(o -> o.id().equals(order.id()));
// Non-capturing — one object for the whole program.
process(o -> o.id() != null);
}The this difference
public class Scoping {
private String name = "outer";
void anonymous() {
Runnable r = new Runnable() {
private String name = "inner";
@Override public void run() {
System.out.println(this.name); // "inner" — this is the Runnable
System.out.println(Scoping.this.name); // "outer" — needs qualification
}
};
}
void lambda() {
Runnable r = () -> {
System.out.println(this.name); // "outer" — this is the Scoping instance
};
// A lambda cannot declare a field, and cannot shadow `name` with a
// local of the same name — the enclosing scope is shared, not nested.
}
}A lambda does not introduce a new lexical scope. It shares the enclosing method's scope for this,
for parameter names, and for shadowing. That last part bites when converting an anonymous class to a
lambda: a parameter name that was legal inside the anonymous class becomes a duplicate-variable
compile error in the lambda.
It also means a lambda cannot be recursive by referring to itself — there is no name to refer to. The workaround is a field or an array holding the reference, or simply writing a method.
Effectively final, precisely
int total = 0;
list.forEach(x -> total += x); // does not compile
// The compiler's objection is not "mutation is bad"; it is that `total`
// lives on the stack frame of a method that may already have returned by
// the time the lambda runs. The lambda holds a COPY of the value.
// Instance fields are captured through `this`, so they have no restriction:
class Counter {
int total = 0;
void sum(List<Integer> list) {
list.forEach(x -> total += x); // compiles — but not thread-safe
}
}The usual escape hatches — an AtomicInteger, a one-element array, a mutable holder — all work by
capturing a reference to a mutable container rather than a value. They compile, and they are almost
always a signal that the loop wanted to be a reduce, a count(), or a Collectors.summingInt.
Method references, and one subtlety
String::length and s -> s.length() compile to nearly the same thing, and the method reference is
usually clearer. One case is genuinely different:
List<String> list = getList();
Runnable a = list::size; // `list` is evaluated NOW and captured
Runnable b = () -> list.size(); // `list` is read when the lambda RUNS
// If `list` were a field reassigned in between, a and b would disagree.With object::method, the receiver expression is evaluated at the point the reference is created.
With the equivalent lambda, it is evaluated at invocation. For a local variable this is
indistinguishable — it is effectively final — but for a field or an expression with side effects, it
is not.
The answer to give
"A lambda is not an anonymous class. javac lifts the body into a private static method and emits
invokedynamic; LambdaMetafactory generates the implementing class at first call and links the
call site, so there is no extra class file, non-capturing lambdas are singletons, and this refers to
the enclosing instance rather than a new object."
Then be ready for: which allocates more, why locals must be effectively final, and what this means
in each. Those three follow-ups cover the whole question.
Frequently Asked Questions
Is a lambda just syntactic sugar for an anonymous inner class?
What does this refer to inside a lambda?
Why must captured local variables be effectively final?
Related tutorials
- Java 8 Features: The Interview OverviewEvery Java 8 feature an interviewer asks about, why each was added, and the one-sentence answer for each — lambdas, streams, Optional, default methods, java.time, CompletableFuture and Metaspace.
- Functional Interfaces: Function, Predicate, Supplier, ConsumerThe four core shapes and how to derive the other thirty-nine, why primitive specialisations exist, compose versus andThen, and the checked-exception problem with a clean workaround.
- Stream API Fundamentals: Lazy PipelinesHow a stream pipeline actually executes: why nothing runs until the terminal operation, what short-circuiting really means, stateful versus stateless operations, and why a stream is single-use.
- Collectors, groupingBy and Downstream CollectorsThe collector API in depth: multi-level groupingBy, downstream collectors, the toMap duplicate-key exception, the null-value trap, teeing and flatMapping, and writing a Collector by hand.