Topic 10.1
Lambda Expressions
In one line
A lambda expression is a short, nameless function you can store in a variable or pass to a method, like (a, b) -> a + b. It always becomes an object of a functional interface type, and it can read local variables only if they are effectively final.
Think of it like this
When you leave your little brother with a babysitter, you don't hire a new person for every instruction. You hand over a sticky note: "if he's hungry, give him a sandwich". The note is a small piece of behaviour that someone else will carry out later, when the moment comes. A lambda is that sticky note in Java: a small piece of code you hand to another method, which decides when (and how often) to run it.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Lambda expression
- A short function without a name, written as
parameters -> body, that you can store in a variable or pass to a method. - Behaviour parameter
- A method parameter that receives code to run (like a comparison rule) rather than plain data.
- Anonymous class
- A class without a name, declared and instantiated in one expression with
new Interface() { ... }(Topic 5.13). Lambdas replace most of them. - Functional interface
- An interface with exactly one abstract method. A lambda can only be turned into an object of such an interface.
- Target type
- The type the compiler expects at the place where the lambda is written, for example the parameter type of the method you're calling. It decides what the lambda means.
- Effectively final
- A local variable that is never reassigned after it gets its value, even though it isn't marked
final. - Capture
- When a lambda uses a variable from the surrounding method, it copies that variable's value into itself. That's called capturing it.
- invokedynamic
- A JVM bytecode instruction (added in Java 7) whose real target is decided at runtime by a bootstrap method. Java uses it to create lambda objects.
Step by step
01The problem lambdas solve
Many methods need a rule from you: list.sort needs to know how to compare, a thread needs to know what to run, a button needs to know what to do when clicked. Java passes rules as objects that implement an interface.
Before Java 8 that object came from an anonymous class. Look how much of this snippet is ceremony: the new, the interface name, the generic type twice, @Override, public, the return type and parameter types. Only the return line is the actual idea.
// Java 7 and earlier
names.sort(new Comparator<String>() {
@Override
public int compare(String a, String b) {
return Integer.compare(a.length(), b.length());
}
});
// Java 8 and later: the same rule
names.sort((a, b) -> Integer.compare(a.length(), b.length()));02Anatomy of a lambda
A lambda has three parts: a parameter list, the arrow ->, and a body. The parameter types are usually left out because the compiler already knows them from the target type.
If the body is a single expression, its value is returned automatically and you write no return and no braces. If you need several statements, use braces and return like a normal method. Mixing the two (x -> { x * 2 }) is an error: inside braces, x * 2 is a statement that does nothing, and the method is missing its return value.
Runnable r = () -> System.out.println("hi"); // no parameters, void body
Function<Integer, Integer> dbl = x -> x * 2; // one parameter, no brackets needed
BinaryOperator<Integer> add = (a, b) -> a + b; // two parameters need brackets
BinaryOperator<Integer> mul = (Integer a, Integer b) -> a * b; // explicit types: all or none
BinaryOperator<Integer> sub = (var a, var b) -> a - b; // Java 11: var in lambda parameters
Function<Integer, String> label = n -> { // block body
String size = n > 10 ? "big" : "small";
return n + " is " + size; // return is required here
};03Target typing: the context decides what a lambda is
The text x -> x * 2 means nothing on its own. Placed where a Function<Integer, Integer> is expected, it becomes a Function; where an IntUnaryOperator is expected, it becomes an IntUnaryOperator. The compiler looks at the target interface, finds its single abstract method, and checks the lambda against that method's parameters and return type.
Lambdas can appear wherever there's a target type: an assignment, a method argument, a return statement, a cast like (Runnable) () -> {}, or the branches of a ?:. They can't appear where there's none, like var f = () -> 42; or Object o = () -> 42; (Object isn't a functional interface).
04Capturing local variables: why effectively final
A lambda often runs later, maybe after the method that created it has returned and its stack frame (Topic 3.11) is gone. So the lambda can't point at the local variable's slot on the stack; it copies the value into the lambda object on the heap when the lambda is created.
If Java let you reassign the variable afterwards, the copy inside the lambda and the variable outside would disagree, and in multi-threaded code you'd get data races on locals. The rule "captured locals must be effectively final" makes the copy indistinguishable from the original.
Fields are different: an instance lambda captures this, and reads the field through it every time, so field changes are visible. The same goes for the contents of a captured array or list: the reference is copied, the object it points to is shared.
05A lambda shares the enclosing scope
An anonymous class body is a new class, so it has its own this and its own names. A lambda body is just a block inside the enclosing method. So this inside a lambda refers to the enclosing object, toString() calls the enclosing class's method, and a lambda parameter named like an existing local is a compile error: variable s is already defined in method main(String[]).
This makes lambdas more predictable than anonymous classes. If you genuinely need a separate object with its own state and several methods, you still need a class.
06What the compiler really produces
Compile a class with two lambdas and look inside with javap -p (show private members). There is no Main$1.class like an anonymous class would create. Instead Main gained two private static methods holding the lambda bodies. The capturing lambda's method takes an extra first parameter: that's the captured bonus.
In main, each lambda expression became one invokedynamic instruction. On first execution, the JVM calls the bootstrap method LambdaMetafactory.metafactory, which generates a hidden class implementing IntUnaryOperator whose applyAsInt calls lambda$main$0. Later executions reuse the linked call site.
07Keep lambdas short
A lambda is at its best when it fits on one line and its meaning is obvious. A ten-line lambda inside a stream pipeline hides logic that deserves a name. Move it into a private method with a descriptive name and refer to it with a method reference (this::isEligible, Topic 10.3).
Stack traces also get easier to read: an exception thrown inside a lambda shows a frame like at Main.lambda$main$0(Main.java:7), while a named method shows its real name.
Try it yourself
- 1
Predict the sort
In the first example, change
byLength.reversed()tobyLength.thenComparing((a, b) -> a.compareTo(b)). Predict the list before you run it: what happens toAlandBonow that ties are broken alphabetically? - 2
Break the capture rule
In the capture example, add
base = 200;on the line afterSupplier<Integer> snapshot = .... Predict the compiler message (hint: it points atbaseinside the lambda, not at the assignment), then run it. Remove the line again. - 3
Write your own behaviour parameter
Add a method
static void repeat(int times, Runnable action)that runsactiontimestimes. Call it asrepeat(3, () -> System.out.println("hip hip hooray")). Then call it with a lambda that incrementscounter[0]and print the counter.
Code & diagrams
List.sort is stable: Al and Bo have the same length, so they keep their relative order in both sorts.
Expected output
anonymous class: [Al, Bo, Ravi, Meera]
lambda, reversed: [Meera, Ravi, Al, Bo]
Hello from a lambda
Hello from a lambdaapplyTwice doesn't know what f does. The caller decides, by passing different lambdas.
Expected output
no parameters
12
5
20
5
42 is big
applyTwice: 18
applyTwice inline: 201The int[] trick works but hides shared mutable state. In real code prefer a return value, a reduction (Topic 10.5) or an AtomicInteger (Topic 13.8).
Expected output
lambda this: field of Main / local variable
anonymous this: field of the anonymous class / local variable
counter: 3
captured value: 101Reusing one object for a non-capturing lambda is what OpenJDK does today; the language spec allows it but doesn't promise it. Never rely on lambda identity in real code.
Expected output
anonymous class name: Main$1
lambda class name starts with Main$$Lambda: true
non-capturing, same object each time: true
capturing, same object each time: false
capture(21) gives 42Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Change a captured local variable
Inside main, write int count = 0; Runnable r = () -> count++;.
Break #2
Give a lambda to var
Write var f = () -> 42;.
Break #3
Reuse a local name as a lambda parameter
Write String s = "hello"; Function<String, Integer> len = s -> s.length();.
Myth vs fact
Myth
A lambda is just shorter syntax for an anonymous class.
Fact
They behave differently: a lambda doesn't create a new scope (this is the enclosing object), can't shadow locals, has no own state, isn't compiled to a separate .class file, and is created through invokedynamic, often reusing one object when it captures nothing.
Myth
Lambdas can only use variables declared final.
Fact
They can use any local that is effectively final (never reassigned), with or without the final keyword. Fields and the contents of captured objects can change freely.
Myth
Every evaluation of a lambda allocates a new object.
Fact
A non-capturing lambda is usually linked to a single cached instance. Capturing lambdas allocate (to hold the captured values), but the JIT often removes even that allocation through escape analysis when the lambda doesn't escape.
Myth
Lambdas are slower than loops and anonymous classes.
Fact
After warm-up, a lambda call is an interface call that the JIT inlines like any other. The real costs are a small one-time linkage cost on first execution, and boxing if you use Function<Integer, Integer> where IntUnaryOperator would do.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Translation: javac desugars the body into
private staticmethodlambda$<enclosing>$<n>(an instance method if the body usesthis), with captured values as leading parameters. The call site isinvokedynamicwith bootstrapLambdaMetafactory.metafactory, passing the interface method type, aMethodHandleto the body and the instantiated type. This design (JSR 335) let the JDK change the lambda strategy without recompiling user code. - ▸
Since Java 15 the metafactory defines lambda classes as hidden classes (
Lookup.defineHiddenClass), which can't be found by name and can be unloaded with their defining class. You can still dump them for inspection with-Djdk.invoke.LambdaMetafactory.dumpProxyClassFiles(newer JDKs) or-Djdk.internal.lambda.dumpProxyClasses=<dir>(Java 8 to 20). - ▸
Serializable lambdas (
(Runnable & Serializable) () -> ...) are compiled withaltMetafactoryand a synthetic$deserializeLambda$method; they serialize to aSerializedLambdathat names the implementation method. That ties the stream to private method names, so serialized lambdas break across recompiles. Avoid them. - ▸
Bootstrapping many lambdas costs startup time (each first
invokedynamiclinks a call site and spins a class). The JDK mitigates it with CDS archives of pre-generated lambda forms, and Project Leyden's AOT caches go further. In very hot paths, prefer primitive specialisations (IntPredicate) to avoid boxing per call.
Remember this
- 1
Before Java 8, passing behaviour meant writing an anonymous class:
new Comparator<String>() { public int compare(String a, String b) { ... } }. Five lines of ceremony around one line of logic. A lambda expression keeps only the logic:(a, b) -> Integer.compare(a.length(), b.length()). Parameters on the left, the arrow->, and the body on the right. - 2
A lambda has no type of its own. The compiler gives it a type from the place it appears, called its target type, and that target must be a functional interface: an interface with exactly one abstract method, like
Runnable,Comparator<T>orFunction<T, R>(Topic 10.2). The lambda becomes the body of that one method. That's whyvar f = () -> 42;doesn't compile: withvarthere's no target type to infer from. - 3
Syntax forms:
() -> 42(no parameters),x -> x * 2(one parameter, brackets optional),(a, b) -> a + b(several),(int a, int b) -> a + b(explicit types),(var a, var b) -> a + b(Java 11, useful to add annotations), and a block body{ ... return ...; }when you need several statements. An expression body returns its value automatically; a block body needsreturnunless the method isvoid. - 4
A lambda can use local variables from the method around it, but only if they are effectively final: never reassigned after their first assignment. The lambda receives a copy of the value at the moment it is created, so allowing later changes would make the copy and the original silently disagree. Fields and array contents are not copied (the lambda reaches them through a reference), so they can change.
- 5
A lambda is not a new scope the way an anonymous class is. Inside it,
thismeans the enclosing object, not the lambda, and you can't declare a parameter or local with the same name as a local of the enclosing method. An anonymous class, by contrast, is a real class: itsthisis the anonymous object, and its own fields hide outer names. - 6
Under the hood the compiler moves the lambda body into a private method (
lambda$main$0) and emits aninvokedynamicinstruction. The first time it runs, the JVM'sLambdaMetafactoryspins up a small hidden class implementing the interface. No.classfile appears per lambda, and lambdas that capture nothing are typically created once and reused.
Explain it without notes
What is a lambda expression, and what decides its type?
Why must captured local variables be effectively final, while fields can change?
How does this behave inside a lambda compared with an anonymous class?
How does the JVM create a lambda object at runtime?
Practice
Write static void repeat(int times, Runnable action) and use it to print Hello 1, Hello 2, Hello 3 using a counter held in an int[].
Sort List.of("kiwi", "fig", "banana", "apple") (copied into an ArrayList) by length, and alphabetically for equal lengths, using one lambda comparator. Print the list.
Write static Function<Integer, Integer> adder(int n) that returns a lambda adding n. Create add5 and add10 and print add5.apply(1), add10.apply(1) and add5.apply(add10.apply(0)).
Trade-offs
- ↔
Lambdas make behaviour cheap to pass around, which encourages small composable functions, but a long chain of anonymous lambdas can be harder to debug than a loop: stack traces show
lambda$main$3, and you can't easily set a breakpoint on "the third filter". Name complex logic as methods. - ↔
Capturing only effectively final values keeps lambdas safe to run later or on other threads, but it forces you to restructure code that wants to accumulate into a local. That friction is deliberate: it pushes you towards reductions and collectors instead of shared mutable state.
- ↔
Anonymous classes still win when you need state, several methods, or a distinct
this(for example an abstract class with constructor arguments). Lambdas only target interfaces with one abstract method.
Done when you can
Done when you can write a lambda in every syntax form and know when
returnand braces are needed.Done when you can explain target typing and why
var f = () -> 1fails.Done when you can explain effectively final capture and work around it correctly.
Done when you can contrast
thisin a lambda and in an anonymous class.Done when you can describe how
invokedynamicandLambdaMetafactorycreate a lambda object.