Introduced in Java 8, they eliminate much of the boilerplate required for anonymous classes and enable a functional programming style.
Lambda expressions are primarily used to implement functional interfaces and form the foundation of the Stream API, method references, and many modern Java APIs.
What is a Lambda Expression?
A lambda expression is an anonymous function that can be passed as data, assigned to variables, or returned from methods. It consists of a parameter list, an arrow operator (->), and a function body.
(parameters) -> expression
(parameters) -> {
// Method body
}
Unlike regular methods, lambda expressions do not have a name, return type, or access modifiers.
Why Lambda Expressions?
Before Java 8, behavior was commonly passed using anonymous classes.Runnable task = new Runnable() {
@Override
public void run() {
System.out.println("Running");
}
};
Using a lambda expression:
Runnable task =
() -> System.out.println("Running");
Lambda expressions significantly reduce boilerplate while improving readability.
Lambda Syntax
A lambda expression consists of three parts.1. Parameters
2. Arrow Operator (
->)3. Body
(a, b) -> a + b
If the body contains a single expression, its value is returned automatically.
No Parameter Lambda
Runnable task =
() -> System.out.println("Hello");
Single Parameter Lambda
Parentheses may be omitted when there is a single parameter.Consumer<String> consumer =
name -> System.out.println(name);
Both forms are valid.
(name) -> System.out.println(name)
name -> System.out.println(name)
Multiple Parameters
Multiple parameters require parentheses.BinaryOperator<Integer> add = (a, b) -> a + b;
Expression Body
Single-expression lambdas return the expression automatically.Function<Integer, Integer> square = number -> number * number;
No explicit return statement is required.
Block Body
Multiple statements require braces.Consumer<String> printer =
name -> {
System.out.println(name);
System.out.println(name.length());
};
A return statement becomes mandatory for non-void lambdas.
Function<Integer, Integer> cube =
number -> {
return number * number * number;
};
Functional Interfaces
A lambda expression can only implement a functional interface, which contains exactly one abstract method.@FunctionalInterface
interface Calculator {
int calculate(int a, int b);
}
Implementation using a lambda:
Calculator calculator = (a, b) -> a + b;
The compiler automatically maps the lambda to the functional interface method.
@FunctionalInterface
The @FunctionalInterface annotation tells the compiler that an interface must contain exactly one abstract method.@FunctionalInterface
interface Printer {
void print(String message);
}
Compilation fails if additional abstract methods are added. The annotation is optional but recommended.
Built-in Functional Interfaces
The java.util.function package provides commonly used functional interfaces.Predicate<T>
Function<T, R>
Consumer<T>
Supplier<T>
UnaryOperator<T>
BinaryOperator<T>
These interfaces eliminate the need to define custom functional interfaces for common operations.
Predicate
A Predicate accepts one argument and returns a boolean.Predicate<Integer> even = number -> number % 2 == 0;
System.out.println(even.test(10));
Output:
true
Predicates are commonly used for filtering.
Function
A Function transforms one value into another.Function<String, Integer> length = String::length;
System.out.println(length.apply("Java"));
Output:
4
Consumer
A Consumer accepts a value but returns nothing.Consumer<String> printer = System.out::println;
printer.accept("Java");
Consumers typically perform side effects such as logging or printing.
Supplier
A Supplier returns a value without accepting any input.Supplier<UUID> supplier = UUID::randomUUID;
System.out.println(supplier.get());
UnaryOperator
A UnaryOperator accepts and returns the same type.UnaryOperator<String> upper = String::toUpperCase;
BinaryOperator
A BinaryOperator accepts two values of the same type and returns the same type.BinaryOperator<Integer> multiply = (a, b) -> a * b;
Variable Capture
Lambda expressions can access variables from the enclosing scope.String prefix = "Hello";
Consumer<String> printer =
name -> System.out.println(prefix + name);
Captured local variables must be final or effectively final.
The following is invalid.
int count = 0;
Consumer<String> printer =
value -> System.out.println(count);
count++;
The compiler reports an error because count is modified.
this in Lambda Expressions
Inside a lambda, this refers to the enclosing class instance.class Demo {
void execute() {
Runnable task = () -> System.out.println(this);
}
}
This differs from anonymous classes, where this refers to the anonymous class instance.
Method References
A method reference is a shorthand for a lambda that simply invokes an existing method.names.forEach(System.out::println);
Equivalent lambda:
names.forEach(
name -> System.out.println(name)
);
Method references improve readability and reduce unnecessary code.
Types of Method References
Java supports four kinds of method references.1. Static method reference.
Integer::parseInt
2. Instance method of a particular object.
printer::print
3. Instance method of an arbitrary object.
String::length
4. Constructor reference.
Employee::new
Constructor references create objects using existing constructors.Supplier<Employee> supplier = Employee::new; Employee employee = supplier.get();
Function Composition
TheFunction interface supports function composition using andThen() and compose().
Function<Integer, Integer> square = number -> number * number;
Function<Integer, Integer> addTen = number -> number + 10;
Function<Integer, Integer> result = square.andThen(addTen);
System.out.println(result.apply(5));
Output:
35
Final Notes
Lambda expressions introduced a concise and expressive way to represent behavior in Java.They simplify the implementation of functional interfaces, reduce boilerplate, and enable a functional programming style that integrates seamlessly with streams, collections, and other modern Java APIs.