Lambdas and functional interfaces
What a lambda compiles to, the four core functional interfaces, method references, and effectively-final variables.
A lambda is a short way to write an object with one method. That is all it is at the language level, and keeping that in mind explains every rule about it: what it can capture, what it compiles to, why this means what it means inside one, and why a lambda that throws a checked exception is such a nuisance. This lesson is those rules, then the machinery under them: the invokedynamic instruction, the class that is spun at run time, and the difference between a lambda that costs nothing and one that allocates on every evaluation.
From anonymous class to lambda
Comparator<String> byLength = new Comparator<String>() {
@Override public int compare(String a, String b) {
return Integer.compare(a.length(), b.length());
}
};
Comparator<String> byLength = (a, b) -> Integer.compare(a.length(), b.length());Both produce a Comparator. The lambda's parameter types are inferred from the target type — the compiler knows a Comparator<String> is wanted, sees its single abstract method takes two strings, and types a and b accordingly. A lambda has no type of its own; it takes the type of whatever functional interface it is assigned or passed to.
Syntax variants: x -> x * 2 (one parameter, no parentheses), () -> 42 (none), (int x, int y) -> x + y (explicit types), x -> { log(x); return x; } (block body with return).
Functional interfaces
Any interface with exactly one abstract method is a lambda target. java.util.function supplies the standard ones:
| Interface | Signature | Meaning |
|---|---|---|
Supplier<T> | () → T | produces a value |
Consumer<T> | T → void | accepts a value |
Function<T,R> | T → R | transforms |
Predicate<T> | T → boolean | tests |
UnaryOperator<T> | T → T | transforms to the same type |
BiFunction<T,U,R> | (T,U) → R | two inputs |
BinaryOperator<T> | (T,T) → T | combines two |
Plus primitive specialisations — IntPredicate, ToLongFunction, IntBinaryOperator — that avoid boxing. And Runnable and Callable<V> from the concurrency package. Use these unless a domain name carries meaning; then declare your own with @FunctionalInterface.
Composition is built in: predicate.negate(), p1.and(p2), f1.andThen(f2), f1.compose(f2), Function.identity(), Comparator.comparing(...).thenComparing(...).
Under the hood: invokedynamic and the hidden class
javac does not compile a lambda into a class. It does two things. The body becomes a private synthetic method on the enclosing class, lambda$main$0, static if the body does not use this and an instance method if it does. The creation site becomes one invokedynamic instruction whose bootstrap is LambdaMetafactory.metafactory, carrying three static arguments: the erased method type of the interface method, a method handle to the synthetic body, and the instantiated (generic) method type.
private static int lambda$main$0(java.lang.String, java.lang.String); // the body, an ordinary method
…
invokedynamic #0:compare:()Ljava/util/Comparator; // the creation site
// BootstrapMethods: LambdaMetafactory.metafactory(…, lambda$main$0, …)The first time that instruction executes, the JVM calls the bootstrap. LambdaMetafactory spins a hidden class in memory (Lookup.defineHiddenClass since Java 15) that implements Comparator and whose compare calls lambda$main$0, and links the call site to a constructor or a constant. The call site is then permanently bound; the bootstrap never runs again for that site. javap shows no class file for it, and stack traces name it Main$$Lambda/0x….
Two consequences decide cost. A non-capturing lambda (it uses no locals from the enclosing scope) is linked to a constant: every evaluation of that expression returns the same instance, allocating nothing. A capturing lambda is linked to the hidden class's constructor: each evaluation allocates a small object holding the captured values as fields, which is exactly what an anonymous class did, minus the class file. The JIT sees through both: at a monomorphic call site it inlines the interface call into the synthetic body, so list.forEach(x -> …) compiles to a loop with the body in it.
Method references use the same mechanism with the method handle pointing at the target method directly, no synthetic body needed; String::length costs the same as s -> s.length(), and a bound reference like prefix::concat captures prefix. The first execution of any lambda site costs the bootstrap, tens of microseconds; a startup with ten thousand distinct lambda sites pays that ten thousand times, which is why the JDK archives lambda linkage in the CDS archive (JEP 350's dynamic archive covers it).
Method references
When a lambda only calls one method, name the method:
| Form | Lambda | Reference |
|---|---|---|
| Static | s -> Integer.parseInt(s) | Integer::parseInt |
| Instance, bound | s -> prefix.concat(s) | prefix::concat |
| Instance, unbound | s -> s.toUpperCase() | String::toUpperCase |
| Constructor | () -> new ArrayList<>() | ArrayList::new |
The unbound form takes the receiver as the first parameter: String::compareToIgnoreCase is a Comparator<String>. When a reference is ambiguous between overloads, write the lambda. One subtlety of the bound form: prefix::concat evaluates prefix once, when the reference is created, and captures the result; a lambda s -> prefix.concat(s) reads prefix on every call. If prefix is a field that changes, the two differ.
Capture and effectively final
A lambda can use local variables from the enclosing scope only if they are effectively final — never reassigned after initialisation:
int count = 0;
items.forEach(i -> count++); // does not compile: count is reassignedThe reason is the stack: the lambda may run after the method returns, when the local is gone, so the value is copied into the lambda at creation — the diagram's "captured values" fields. A copy of a variable you then mutate would silently diverge; Java forbids it. The fixes are to compute with a stream (items.size(), mapToInt(...).sum()), or to use a mutable holder (AtomicInteger, an array of one) when you genuinely need mutation — and to notice that the second is usually a sign a loop would be clearer.
Fields and captured objects are not restricted: the lambda holds the reference, and mutating the object through it is allowed (and is the same thread-safety question as anywhere).
this inside a lambda
In an anonymous class, this is the anonymous instance. In a lambda, this is the enclosing instance — a lambda is not an object with its own identity from the language's point of view. That is usually what you want (this.repository.save(...) inside a map), and it means a lambda that uses this (or a field, or an instance method) captures it, which keeps the enclosing object alive as long as the lambda is. A lambda that uses no instance state does not capture this, which is the one way a lambda is lighter than an anonymous class, which always does.
Walkthrough: the listener that kept the request alive
A metrics library keeps a List<Runnable> onFlush for the lifetime of the process. A request handler registered a callback:
class OrderHandler {
private final byte[] payload = new byte[2_000_000]; // the parsed request, 2 MB
void handle() {
metrics.onFlush(() -> log.info("flushed")); // uses nothing from this... or does it?
}
}logis an instance field. The lambda reads it, sojavaccompiles the body as an instance synthetic method and theinvokedynamiccapturesthis: the hidden class holds a reference to theOrderHandler.- The
OrderHandlerholds a 2 MB payload. TheonFlushlist holds the lambda; the lambda holds the handler; the handler holds the payload. One request, 2 MB pinned in the old generation forever. - At 50 requests per second the heap grows by 100 MB a second until the first full GC, which reclaims nothing, and the service dies with an
OutOfMemoryErroran hour after deploy. The heap dump shows two millionbyte[]s reachable from aListin the metrics library, and the path throughOrderHandler$$Lambda. - The fix is to make the lambda not capture
this:metrics.onFlush(() -> LOG.info("flushed"))with a static logger, or pass only what it needs,var l = log; metrics.onFlush(() -> l.info(...)), which captures the logger and not the handler. Or unregister on completion. The rule: a lambda handed to something long-lived must capture only what is small.
An anonymous class here would have leaked the same way and been easier to spot, because Foo$1 has a visible this$0 field. The lambda's capture is invisible in the source, and javap -p (is the synthetic method static?) or a heap dump is how you see it.
Checked exceptions
Function.apply does not declare throws, so a lambda body cannot throw a checked exception:
paths.stream().map(p -> Files.readString(p)) // does not compile: IOExceptionOptions: wrap in a helper that converts to unchecked (UncheckedIOException exists for exactly this), write a small ThrowingFunction interface and an adapter, or use a loop. There is no elegant built-in answer; pick one convention per codebase.
Try it yourself
Same object or not?
Supplier<String> a = () -> "x";
Supplier<String> b = () -> "x";
String p = "pre";
Supplier<String> c1 = () -> p + "!";
Supplier<String> c2 = () -> p + "!";
Runnable r1 = () -> System.out.println("hi"), r2 = () -> System.out.println("hi");
System.out.println((a == b) + " " + (c1 == c2));What prints, and does evaluating () -> "x" twice at the same site give the same object?
Answer
false false. a and b are two different call sites, each with its own hidden class and its own singleton, so they differ. c1 and c2 capture p and allocate per evaluation, so they differ too. The same non-capturing site evaluated twice (in a loop, say) returns the same instance both times, because the site was linked to a constant. None of this is specified; the JLS says lambda identity is unpredictable, and code must never compare lambdas with ==.
Static or instance?
For each lambda inside class Svc { Logger log; int limit = 5; }, say whether the synthetic method is static and whether this is captured:
(a) x -> x > 5, (b) x -> x > limit, (c) x -> log.info(x), (d) x -> Svc.check(x) where check is static.
Answer
(a) static, no capture. (b) instance, captures this (reads the field limit). (c) instance, captures this (reads log). (d) static, no capture. Only (b) and (c) keep the Svc alive from wherever the lambda is stored, and only (a) and (d) are shared singletons. The test is whether the body touches any instance state, not whether it mentions this.
Bound now or later?
class Greeter {
String prefix = "Hello";
Function<String, String> byRef = prefix::concat;
Function<String, String> byLambda = s -> prefix.concat(s);
}
var g = new Greeter(); g.prefix = "Bye";
System.out.println(g.byRef.apply("!") + " " + g.byLambda.apply("!"));Answer
Hello! Bye!. A bound method reference evaluates its receiver expression once, when created (during construction, when prefix was "Hello"), and captures that String. The lambda reads the field each time it runs and sees "Bye". They are equivalent only when the receiver never changes, which is why this::method and obj::method are safe for immutable receivers and a trap for mutable fields.
Misconceptions
- "A lambda is an anonymous inner class with shorter syntax." It is an
invokedynamicsite linked to a hidden class spun at run time, with no class file, no capture ofthisunless used, and a shared instance when it captures nothing. - "Lambdas allocate on every call." Non-capturing ones allocate once per site, ever. Capturing ones allocate per evaluation, like any object, and escape analysis removes many of those.
- "
thisin a lambda is the lambda." It is the enclosing instance. There is no lambdathisto refer to. - "Effectively final is a style rule." It is a consequence of capture by value: the local is copied into the lambda object, and mutation would make two variables pretend to be one.
- "Method references are always equivalent to the lambda." A bound reference captures its receiver at creation; a lambda re-reads it per call. Different behaviour for a mutable receiver.
Going deeper
- JLS §15.27 (lambda expressions), including §15.27.4 on the unspecified identity and class of a lambda object.
java.lang.invoke.LambdaMetafactoryJavadoc, and Brian Goetz, "Translation of Lambda Expressions", which explains whyinvokedynamicwas chosen over inner classes.javap -c -pon any class with a lambda: find the synthetic method and theBootstrapMethodsattribute.- JEP 371 (hidden classes), which is what lambda classes are since Java 15.
- Effective Java, items 42 to 44.