Lambda Expressions
Overview
Lambda Expressions were the headline feature of Java 8 and fundamentally changed how Java code is written. Before Java 8, passing behavior (a function) as a parameter to another method required creating a verbose anonymous inner class. Lambdas provide a clean, concise syntax to do this with minimal boilerplate.
A Lambda Expression is, at its core, an anonymous function — a function without a name, without a class, and without a return statement (for single expressions). The arrow syntax (parameters) -> expression defines the behavior inline. Lambdas can be used wherever a Functional Interface is expected. A Functional Interface is any interface that has exactly one abstract method (like Runnable, Comparator, or any @FunctionalInterface annotated interface).
Java 8 also introduced the java.util.function package with four primary built-in Functional Interfaces:
- `Predicate<T>`: Takes a T, returns a boolean (test(T t)).
- `Function<T, R>`: Takes a T, returns an R (apply(T t)).
- `Consumer<T>`: Takes a T, returns nothing (accept(T t)).
- `Supplier<T>`: Takes nothing, returns a T (get()).
Syntax
Notice that with Predicate, Function, Consumer, and Supplier you can express almost any transformation or check on data. These are the building blocks of the Streams API.
import java.util.*;
import java.util.function.*;
public class Main {
public static void main(String[] args) {
// 1. Before Lambda: Verbose Anonymous Class
Comparator<String> oldWay = new Comparator<String>() {
@Override
public int compare(String a, String b) {
return a.compareTo(b);
}
};
// 2. After Lambda: Clean and Concise
Comparator<String> newWay = (a, b) -> a.compareTo(b);
// 3. The 4 Core Functional Interfaces
// Predicate<T>: Takes T, returns boolean
Predicate<Integer> isEven = n -> n % 2 == 0;
System.out.println(isEven.test(4)); // true
// Function<T, R>: Takes T, returns R
Function<String, Integer> strToLength = s -> s.length();
System.out.println(strToLength.apply("Java")); // 4
// Consumer<T>: Takes T, returns nothing
Consumer<String> printer = s -> System.out.println("-> " + s);
printer.accept("Hello Lambda!"); // -> Hello Lambda!
// Supplier<T>: Takes nothing, returns T
Supplier<String> greeting = () -> "Good morning!";
System.out.println(greeting.get()); // Good morning!
// 4. Practical: Sorting a list with Lambda
List<String> names = new ArrayList<>(List.of("Charlie", "Alice", "Bob"));
names.sort((a, b) -> a.compareTo(b));
System.out.println(names); // [Alice, Bob, Charlie]
}
}Common Pitfalls
- Using lambdas where a method reference would be clearer.
s -> System.out.println(s)can and should be written as the method referenceSystem.out::println. Method references are more readable and compile to slightly more efficient bytecode. - Trying to modify local variables from the enclosing scope. Lambdas can only read local variables from the surrounding method if those variables are
finalor effectively final (never reassigned). Trying to writeint count = 0; list.forEach(e -> count++);will fail to compile becausecountis being mutated. - Catching checked exceptions inside lambda expressions. If a lambda body calls a method that throws a Checked Exception, the lambda must handle it, but it cannot declare
throwsin its signature. The common workaround is to wrap checked exceptions in a RuntimeException, or use a helperFunctionWithExceptionwrapper interface.
Interview Questions
A Functional Interface is any interface with exactly one abstract method (SAM — Single Abstract Method). Lambdas work by providing the implementation of that single method. The compiler uses the Functional Interface as the target type, resolving exactly which one abstract method the lambda is implementing. If an interface had two abstract methods, the compiler wouldn't know which one the lambda is implementing.
A Method Reference (ClassName::methodName) is a more concise notation for a lambda that simply calls an existing method without any additional logic. s -> s.toUpperCase() is functionally identical to String::toUpperCase. Method references are preferred when the lambda body contains only a single method call on the parameter.
They are effectively objects at runtime, but not created via anonymous classes. Lambda expressions are compiled to invokedynamic bytecode instructions (Java 7+ JVM feature). The first time a lambda is invoked, the JVM generates a tiny class for it. Subsequent invocations reuse the generated class. This makes lambdas significantly faster than traditional anonymous inner classes.
Real-World Example
Modern Spring Boot applications use lambdas pervasively in security configuration, routing, and validation. The entire Spring Security DSL is built on chaining lambdas, eliminating hundreds of lines of XML or class-based configuration.
// Spring Security 6 configuration using lambdas
@Bean
public SecurityFilterChain securityChain(HttpSecurity http) throws Exception {
return http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/public/**").permitAll()
.anyRequest().authenticated()
)
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.build();
}Check Your Knowledge
Test your understanding of Lambda Expressions with these quick questions.