Replace anonymous classes with concise lambda syntax — the shift to functional Java.
Published September 21, 2026
A lambda expression (Java 8+) is a short way to write a piece of behaviour as a value, a function you can store in a variable, pass to a method, or return. It replaces the bulky anonymous classes that Java previously needed whenever code had to be passed around: comparators, event handlers, tasks for a thread pool, filters for a stream.
// Before: an anonymous class just to say "compare by length"
Collections.sort(words, new Comparator<String>() {
@Override
public int compare(String a, String b) {
return Integer.compare(a.length(), b.length());
}
});
// With a lambda
words.sort((a, b) -> Integer.compare(a.length(), b.length()));
// With a ready-made comparator factory and a method reference
words.sort(Comparator.comparingInt(String::length));
() -> System.out.println("hi") // no parameters
x -> x * 2 // one parameter: parentheses optional
(a, b) -> a + b // several parameters
(String s, int n) -> s.repeat(n) // explicit types (normally inferred)
(a, b) -> { // block body: statements, explicit return
int sum = a + b;
return sum * sum;
}
A single-expression body returns its value automatically. A block body needs return (unless the result type is void).
A lambda has no type of its own. It takes the type of the functional interface expected where it's used: an interface with exactly one abstract method. The lambda is the implementation of that method, and its parameter and return types must match. This is called target typing.
Runnable task = () -> System.out.println("running"); // void run()
Comparator<String> byLength = (a, b) -> a.length() - b.length(); // int compare(T, T)
The java.util.function package provides the common shapes, so you rarely need to declare your own:
| Interface | Method | Shape | Typical use |
|---|---|---|---|
Supplier<T> | T get() | () → T | Lazily create a value (orElseGet) |
Consumer<T> | void accept(T) | T → nothing | Act on a value (forEach) |
Function<T,R> | R apply(T) | T → R | Transform (map) |
Predicate<T> | boolean test(T) | T → boolean | Filter (filter, removeIf) |
BiFunction<T,U,R> | R apply(T,U) | (T, U) → R | Combine two values |
UnaryOperator<T> | T apply(T) | T → T | Same-type transform (replaceAll) |
BinaryOperator<T> | T apply(T,T) | (T, T) → T | Reduce (reduce, merge) |
There are primitive specializations too (IntPredicate, ToIntFunction, IntUnaryOperator, …). They avoid boxing int into Integer, which matters in hot code.
These interfaces also have helpful default methods for composing behaviour:
Predicate<String> nonEmpty = s -> !s.isEmpty();
Predicate<String> shortWord = s -> s.length() < 5;
Predicate<String> valid = nonEmpty.and(shortWord.negate());
Function<String, String> trim = String::strip;
Function<String, Integer> trimmedLength = trim.andThen(String::length);
Comparator<Person> order = Comparator.comparing(Person::lastName).thenComparing(Person::firstName).reversed();
A lambda can use local variables from its surrounding method, but only if they're effectively final, meaning never reassigned after initialization:
String greeting = "Hello, ";
names.forEach(n -> System.out.println(greeting + n)); // ✅ greeting never changes
int count = 0;
names.forEach(n -> count++); // ❌ compile error: count is modified
Why? A lambda can outlive the method call that created it (it may run later, or on another thread), so it captures the variable's value, not the variable itself. Allowing reassignment would make the captured copy and the original silently diverge, and would invite data races. Fields and array elements can be modified from a lambda, which is exactly why doing so from parallel code is dangerous. To aggregate, use the stream's own operations (count(), sum(), collectors) instead of mutating outside variables.
They look interchangeable, but there are real differences:
this: inside a lambda, this means the enclosing object. Inside an anonymous class, it means the anonymous class instance.invokedynamic call, with no extra .class file per lambda, and the JVM can reuse non-capturing lambda instances. They're generally at least as efficient.Standard functional interfaces don't declare checked exceptions, so a lambda body that calls something throwing IOException won't compile as a Function:
paths.stream().map(p -> Files.readString(p)); // ❌ unhandled IOException
paths.stream().map(p -> { // ✅ handle or wrap it
try { return Files.readString(p); }
catch (IOException e) { throw new UncheckedIOException(e); }
});
For repeated cases, a small helper that wraps a throwing function, or a custom functional interface that declares the exception, keeps pipelines readable.
this::isEligible). The call site then reads like a sentence.DiscountPolicy) makes the code clearer.return or break is sometimes clearer than a stream pipeline.Q: What is a functional interface?
A: An interface with exactly one abstract method. Default and static methods don't count. It's the only kind of type a lambda or method reference can be converted to. @FunctionalInterface asks the compiler to enforce the rule.
Q: Why must captured local variables be effectively final? A: The lambda captures a copy of the variable's value, and it may run after the method has returned or on another thread. Permitting reassignment would make that copy silently stale and invite race conditions, so the language forbids it.
Q: What does this refer to inside a lambda?
A: The instance of the enclosing class, the same as in the surrounding code. In an anonymous inner class, this refers to the anonymous object itself. That's one of the practical differences between the two.
Q: Are lambdas slower than anonymous classes?
A: No. They're implemented with invokedynamic, avoiding a separate class file per lambda, and non-capturing lambdas can be cached as single instances. Any performance differences are negligible compared with the work the lambda does. Boxing (using Function<Integer, Integer> instead of IntUnaryOperator) is a more likely cost.
Q: How do you use a method that throws a checked exception inside a lambda?
A: Catch it inside the lambda and handle it or rethrow it as an unchecked exception (UncheckedIOException), or define a functional interface whose method declares the exception. Standard interfaces like Function don't allow checked exceptions.