Lambda Expressions Deep Dive
Lambdas let you pass behaviour as a value. By the end you'll write (a, b) -> a + b instead of a five-line anonymous class, and wire up the whole java.util.function toolkit with confidence.
Learn Lambda Expressions Deep Dive in our free Java course — a beginner-friendly interactive lesson with worked examples, a practice exercise and a quick…
Part of the free Java course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
What You'll Learn in This Lesson
Before You Start
You should be comfortable with interfaces and anonymous classes . A lambda is just a shorthand way to implement an interface that has a single method — so those two ideas are the foundation everything here is built on.
Real-World Analogy: A Sticky Note of Instructions
Imagine you run a kitchen. Sometimes you don't want to do a task yourself — you want to hand a coworker a quick instruction: "slice these", "throw out anything mouldy", "fetch me a clean plate".
💡 Analogy: A lambda is a sticky note with instructions. Before Java 8 you had to write a whole formal letter (an anonymous class) just to say "sort by price". A lambda is the scribbled note instead: (a, b) -> a.price() - b.price() . The method you hand it to (like sort or forEach ) reads the note and follows it. A functional interface is simply the kind of note the recipient accepts — one with a single, clear instruction on it.
1️⃣ Lambda Syntax & Functional Interfaces
A lambda has two parts split by an arrow: parameters -> body . The left side names the inputs; the right side is what to do with them. If the body is a single expression, its value is returned automatically — no return , no braces.
A lambda doesn't exist on its own — it implements a functional interface : an interface with exactly one abstract method (often called the SAM, for Single Abstract Method ). The annotation @FunctionalInterface is optional, but adding it tells the compiler to fail the build if anyone ever adds a second abstract method — a useful safety net.
2️⃣ The java.util.function Toolkit
You rarely need to write your own functional interface. Java ships with a small set in java.util.function that covers almost every shape of "a bit of behaviour". Learn these five and you can read most modern Java:
Interface
Shape
Method
Use it for
Function<T,R>
T -> R
apply
Transform a value
Consumer<T>
T -> void
accept
Do something (e.g. print)
Supplier<T>
() -> T
get
Produce a value
Predicate<T>
T -> boolean
test
A yes/no test (filtering)
BiFunction<T,U,R>
(T, U) -> R
Combine two inputs
Notice the method name changes per interface: you call .apply() on a Function , .test() on a Predicate , .accept() on a Consumer , and .get() on a Supplier . The example below uses all five.
🎯 Your Turn #1 — Write Two Lambdas
Fill in the two blanks so the Predicate and the Function behave correctly. Run it and check your output against the expected lines in the comments.
3️⃣ Method References & Capturing Variables
When a lambda does nothing but call one existing method, you can replace it with a method reference using :: . It's shorter and often clearer. There are four kinds:
Kind
Reference
Equivalent lambda
Static method
Math::abs
x -> Math.abs(x)
Instance on a class
String::toUpperCase
s -> s.toUpperCase()
Instance on an object
System.out::println
x -> System.out.println(x)
Constructor
ArrayList::new
() -> new ArrayList<>()
Lambdas can also capture local variables from the surrounding method — but only ones that are effectively final : assigned exactly once and never changed afterwards. You don't have to write the final keyword; the variable just has to behave as if it were final.
🎯 Your Turn #2 — Method References
Replace the lambdas with the two method references described in the comments. Method references read more cleanly than the equivalent x -> ... lambda.
4️⃣ Lambdas vs Anonymous Classes
Before lambdas, you implemented a single-method interface with an anonymous class — many lines of ceremony for one line of logic. A lambda collapses all of that:
They are not identical, though. The differences that matter:
- Number of methods: a lambda works only for a single -method interface; an anonymous class can implement many methods or extend a class.
- The this trap: inside an anonymous class, this means the anonymous object. Inside a lambda, this means the enclosing class instance. This catches people out constantly.
- Under the hood: a lambda uses invokedynamic and creates no extra .class file, whereas each anonymous class generates its own.
Rule of thumb: reach for a lambda for single-method behaviour; fall back to an anonymous class only when you need fields, multiple methods, or its own this .
🧩 Mini-Challenge — Capture & Filter
No fill-in-the-blanks this time — just a comment outline. Build a Predicate that captures a threshold and use it to filter a stream. The expected output is in the comments so you can self-check.
Common Errors
- ❌ Mutating a captured variable: int count = 0; list.forEach(x -> count++); won't compile — error: "local variables referenced from a lambda expression must be final or effectively final" . Use AtomicInteger counter = new AtomicInteger(); and counter.incrementAndGet() , or a one-element array int[] count = {0}; .
- ❌ Expecting lambda this to behave like an anonymous class: inside a lambda, this refers to the enclosing object, not the lambda. Code ported from an anonymous class that relied on this meaning "this handler" will silently point somewhere else. If you truly need the inner this , keep the anonymous class.
- ❌ Overusing lambdas: a lambda longer than ~3 lines, or one you copy-paste, belongs in a named method. .map(p -> { /* 12 lines */ }) is unreadable — extract it and use a method reference like .map(this::scoreProduct) instead.
- ❌ Forgetting it's a single-method interface: assigning a lambda to a type that isn't a functional interface gives "incompatible types: ... is not a functional interface" . The target must have exactly one abstract method.
- ❌ Block body without a return : Function<Integer,Integer> f = x -> { x * 2; }; fails — once you use braces you must write return x * 2; . Drop the braces to auto-return: x -> x * 2 .
Pro Tips
💡 Prefer method references when a lambda just forwards to a method: Object::toString beats x -> x.toString() for readability.
💡 Compose predicates for readable filters: isAdult.and(isActive) and isEven.negate() read like English and stay reusable.
💡 Name your behaviour by extracting a long lambda into a method — your stream pipeline then reads as a list of verbs.
📋 Quick Reference
Concept
Syntax
Notes
Lambda (expression)
(a, b) -> a + b
Auto-returns the expression
Lambda (block)
x -> { return x*2; }
Braces need explicit return
Functional interface
@FunctionalInterface
Exactly one abstract method
Static ref
Class instance ref
Object ref
Bound to one object
Constructor ref
Capture
must be effectively final
Assign once; never reassign
❓ Frequently Asked Questions
🎉 Lesson Complete!
You can now write lambdas, recognise the five core functional interfaces, swap in method references, capture variables safely, and explain how a lambda differs from an anonymous class — including the this trap.
Next up: Optionals — combine these lambda skills with Optional to eliminate null pointer exceptions for good.
Practice quiz
What is a functional interface?
- Any interface with default methods
- An interface annotated @Override
- An interface with exactly one abstract method (a SAM)
- An interface with only static methods
Answer: An interface with exactly one abstract method (a SAM). A functional interface has exactly one abstract method — the SAM — which is what a lambda implements.
Given Calculator subtract = (a, b) -> { int r = a - b; return r; }, what does subtract.operate(7, 4) return?
- 3
- 11
- 28
- 1
Answer: 3. The block body computes a - b, so 7 - 4 is 3.
Which method do you call on a Predicate<T>?
- apply
- accept
- get
- test
Answer: test. Predicate<T> returns a boolean via test(); Function uses apply, Consumer uses accept, Supplier uses get.
What does the method reference String::toUpperCase mean as a lambda?
- () -> String.toUpperCase()
- s -> s.toUpperCase()
- String.toUpperCase(s)
- s -> new String(s)
Answer: s -> s.toUpperCase(). An unbound instance method reference on a class means s -> s.toUpperCase() — the element is the receiver.
Why must a local variable captured by a lambda be effectively final?
- Because the lambda captures a snapshot, so reassignment would cause disagreement
- For faster execution
- Because lambdas cannot read locals
- It is only a style rule with no effect
Answer: Because the lambda captures a snapshot, so reassignment would cause disagreement. A lambda may run later or on another thread; Java captures a snapshot, so the variable must never be reassigned.
What is the shape of Supplier<T>?
- T -> R
- T -> void
- () -> T
- T -> boolean
Answer: () -> T. Supplier<T> takes no input and produces a T via get() — a factory.
Inside a lambda, what does 'this' refer to?
- The lambda object itself
- The enclosing class instance
- null
- The functional interface
Answer: The enclosing class instance. Unlike an anonymous class, a lambda's 'this' is the enclosing instance — a classic trap when porting code.
Which is the correct constructor reference for ArrayList?
- ArrayList::create
- new ArrayList()
- () -> ArrayList
- ArrayList::new
Answer: ArrayList::new. ArrayList::new is a constructor reference equivalent to () -> new ArrayList<>().
Why is log.debug("x {}", v) cheaper than building the string with x -> x.toString() concatenation when DEBUG is off — and similarly, why won't 'int count = 0; list.forEach(x -> count++);' compile?
- count is private
- A lambda cannot mutate a captured local — it must be effectively final
- forEach returns void
- count is out of scope
Answer: A lambda cannot mutate a captured local — it must be effectively final. Mutating a captured local breaks the effectively-final rule; use AtomicInteger or a one-element array instead.
A lambda with a block body { ... } that should yield a value must do what?
- Omit the braces
- End with a semicolon only
- Use an explicit return statement
- Use yield
Answer: Use an explicit return statement. Once you use braces, you must write an explicit return. Drop the braces to auto-return a single expression.
Continue this course
- Previous: Previous Lesson
- Next: Next Lesson