Lambdas, functional interfaces & method references
Lambda syntax, Function, Predicate, Consumer, Supplier, composing functions and the four kinds of method reference.
Java 8 brought the biggest change in the language's history: lambda expressions. A lambda is a short, anonymous function you can store in a variable or pass to a method. Lambdas power the Streams API, modern collection methods, CompletableFuture, Spring configuration and much more. This lesson covers lambda syntax, the standard functional interfaces, composing functions, and method references.
From anonymous class to lambda#
Suppose we want to sort strings by length. Before Java 8, you wrote an anonymous class:
With a lambda, all the ceremony disappears:
The compiler knows sort expects a Comparator<String>, which has one abstract method compare(String, String). So the lambda is that method's implementation.
Lambda syntax#
Functional interfaces#
A lambda can be used wherever a functional interface is expected: an interface with exactly one abstract method (default and static methods don't count). The lambda's parameters and return value must match that method.
java.util.function provides the common shapes, so you rarely need to define your own:
Primitive specialisations avoid boxing: IntPredicate, IntFunction<R>, ToIntFunction<T>, IntUnaryOperator, IntBinaryOperator, DoubleSupplier and more. Older interfaces like Runnable, Callable<V> and Comparator<T> are functional too.
Passing behaviour into methods#
The real power comes from writing methods that take functions as parameters:
filter and map don't know what to keep or how to transform; the caller decides. That is behaviour parameterisation, and it's exactly what the Streams API offers out of the box.
Composing functions#
Functional interfaces have default methods for combining them:
Capturing variables: effectively final#
A lambda can use variables from the surrounding method, but only if they are effectively final (assigned once, never changed):
This rule prevents confusing behaviour (and data races) when a lambda runs later or on another thread. Fields and array contents are not restricted, but mutating shared state from lambdas is usually a design smell. Use a stream's count() or a plain loop instead.
Inside a lambda, this refers to the enclosing object, not to the lambda itself (unlike in anonymous classes).
Method references#
When a lambda just calls an existing method, a method reference says it more directly with :::
For the negate() example, Java 11 added a neater helper: Predicate.not(MethodRefs::isAdult).
Use a method reference when it is clearer than the lambda. Person::name beats p -> p.name(), but a lambda is better when you need extra logic or the reference would be obscure.
Defining your own functional interface#
When no standard interface communicates intent, or you need a checked exception, define one:
Common mistakes#
- Mutating local variables from a lambda (effectively-final error).
- Writing huge multi-line lambdas. Extract a named method and use a method reference.
- Using
Function<Integer, Integer>in hot loops whenIntUnaryOperatoravoids boxing. - Swallowing checked exceptions inside lambdas. Standard interfaces can't throw them, so wrap them deliberately (e.g. in
UncheckedIOException). - Overusing lambdas where a simple loop is clearer.
What's next#
Lambdas truly shine with the Streams API, which lets you filter, transform, group and summarise data in expressive pipelines. That's next.
Check your understanding
Quick quiz
1.Which functional interface represents a function that takes a
Tand returns aboolean?2.Why does this fail to compile?
int count = 0; list.forEach(x -> count++);3.Which method reference is equivalent to
s -> s.toUpperCase()?
Finished reading?
Mark this lesson complete to track your progress.