Lambda expressions, introduced in Java 8, provide a concise way to represent anonymous functions—blocks of code that can be passed around and executed later. They simplify what used to require verbose anonymous inner classes, making your code cleaner and easier to read.
A lambda expression consists of three parts:
->
separates parameters from the body.General form:
(parameters) -> expression
or
(parameters) -> { statements; }
Runnable r = () -> System.out.println("Hello, world!");
r.run();
This prints Hello, world!
. Here, the lambda implements the Runnable
interface's single method run()
.
Consumer<String> printer = name -> System.out.println("Name: " + name);
printer.accept("Alice");
This prints Name: Alice
.
BiFunction<Integer, Integer, Integer> adder = (a, b) -> a + b;
System.out.println(adder.apply(3, 5)); // Outputs 8
{}
and explicit return
if needed:Function<Integer, String> converter = num -> {
int doubled = num * 2;
return "Result: " + doubled;
};
System.out.println(converter.apply(4)); // Outputs "Result: 8"
Java can usually infer parameter types from context, so explicit types are optional:
Predicate<String> isEmpty = s -> s.isEmpty();
But you can specify types if you prefer:
Predicate<String> isEmpty = (String s) -> s.isEmpty();
Lambdas can access variables from their enclosing scope, but only if those variables are effectively final—meaning their value does not change after assignment. This restriction prevents unexpected side effects and keeps lambdas predictable.
Example:
int factor = 2;
Function<Integer, Integer> multiplier = x -> x * factor;
System.out.println(multiplier.apply(5)); // Outputs 10
Here, factor
is captured by the lambda. Trying to modify factor
after the lambda is defined will cause a compilation error.
By using lambda expressions, you transform bulky anonymous classes into clean, expressive code blocks. They are a fundamental building block of Java's functional programming capabilities.
Function
, Predicate
, Consumer
, Supplier
Java 8 introduced a set of standard functional interfaces in the java.util.function
package. These interfaces define single abstract methods and serve as targets for lambda expressions and method references. The four core interfaces—Function
, Predicate
, Consumer
, and Supplier
—cover the most common functional programming use cases: transforming data, filtering, performing side effects, and supplying values.
FunctionT, R
A Function
takes an input of type T
and produces a result of type R
. It represents a transformation or mapping operation.
R apply(T t)
Example: Mapping Strings to their lengths
Function<String, Integer> lengthFunction = s -> s.length();
List<String> names = List.of("Alice", "Bob", "Charlie");
List<Integer> lengths = names.stream()
.map(lengthFunction)
.toList();
System.out.println(lengths); // Output: [5, 3, 7]
Here, the Function
maps each string to its length.
PredicateT
A Predicate
tests a condition on an input of type T
and returns a boolean indicating if the input matches the condition.
boolean test(T t)
Example: Filtering even numbers
Predicate<Integer> isEven = num -> num % 2 == 0;
List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6);
List<Integer> evens = numbers.stream()
.filter(isEven)
.toList();
System.out.println(evens); // Output: [2, 4, 6]
This Predicate
filters the stream to include only even numbers.
ConsumerT
A Consumer
performs an action on an input of type T
but does not return a result. It’s typically used for side effects like printing or modifying external state.
void accept(T t)
Example: Printing each element
Consumer<String> printer = s -> System.out.println("Name: " + s);
List<String> fruits = List.of("Apple", "Banana", "Cherry");
fruits.forEach(printer);
This prints each fruit name prefixed by "Name:".
SupplierT
A Supplier
produces or supplies a value of type T
without taking any input. It’s useful for lazy value generation or deferred computation.
T get()
Example: Supplying a random number
Supplier<Double> randomSupplier = () -> Math.random();
System.out.println("Random number: " + randomSupplier.get());
System.out.println("Random number: " + randomSupplier.get());
Each call to get()
generates a new random number.
Imagine processing a list of orders. You could use:
Function<Order, Double>
to extract the total price.Predicate<Order>
to filter only completed orders.Consumer<Order>
to log each order’s details.Supplier<Order>
to generate sample test orders on demand.Together, these interfaces let you build expressive, modular, and reusable pipelines that handle data transformation, filtering, side effects, and deferred generation elegantly.
These core functional interfaces are foundational to Java’s functional programming style and integrate seamlessly with streams and other APIs. Mastering their use will empower you to write concise and powerful code.
Method references are a shorthand syntax in Java for writing lambda expressions that simply call an existing method. They improve code readability by eliminating boilerplate when the lambda's body is just a method call. Introduced in Java 8, method references are closely related to lambdas and can be used wherever a lambda expression is expected.
The general syntax of a method reference is:
ClassName::methodName
This replaces a lambda like x -> ClassName.methodName(x)
when the method call matches the functional interface's signature.
There are four main types of method references:
Syntax: ClassName::staticMethod
Equivalent Lambda: x -> ClassName.staticMethod(x)
Example:
Function<String, Integer> parseInt = Integer::parseInt;
System.out.println(parseInt.apply("42")); // Output: 42
This is equivalent to:
Function<String, Integer> parseInt = s -> Integer.parseInt(s);
Syntax: instance::instanceMethod
Example:
Consumer<String> printer = System.out::println;
printer.accept("Hello, method reference!"); // Output: Hello, method reference!
Equivalent lambda:
Consumer<String> printer = s -> System.out.println(s);
Syntax: ClassName::instanceMethod
Used when the instance is provided at runtime.
Example:
List<String> names = List.of("bob", "alice", "carol");
names.sort(String::compareToIgnoreCase);
System.out.println(names); // Output: [alice, bob, carol]
Equivalent lambda:
names.sort((a, b) -> a.compareToIgnoreCase(b));
Syntax: ClassName::new
Used when you want to instantiate a class using a lambda.
Example:
Supplier<List<String>> listSupplier = ArrayList::new;
List<String> myList = listSupplier.get();
myList.add("Item");
System.out.println(myList); // Output: [Item]
Equivalent lambda:
Supplier<List<String>> listSupplier = () -> new ArrayList<>();
By replacing simple lambdas with method references, you can make your code cleaner and easier to understand. As you use more functional constructs in Java, method references will become a natural tool for writing expressive and concise code.
In this section, we’ll build a simple calculator using Java's functional programming features. We'll use lambda expressions, functional interfaces, and method references to model arithmetic operations like addition, subtraction, multiplication, and division. The calculator will allow dynamic selection of operations by passing functions as parameters.
This approach demonstrates how functional programming promotes flexibility and clean separation of logic by treating operations as first-class functions.
Here is a complete, runnable Java program:
import java.util.HashMap;
import java.util.Map;
import java.util.Scanner;
import java.util.function.BiFunction;
public class LambdaCalculator {
public static void main(String[] args) {
// Define arithmetic operations using lambdas and method references
Map<String, BiFunction<Double, Double, Double>> operations = new HashMap<>();
// Using lambdas
operations.put("+", (a, b) -> a + b);
operations.put("-", (a, b) -> a - b);
// Using method reference for multiplication
operations.put("*", LambdaCalculator::multiply);
// Division with lambda and error check
operations.put("/", (a, b) -> {
if (b == 0) {
throw new ArithmeticException("Cannot divide by zero.");
}
return a / b;
});
Scanner scanner = new Scanner(System.in);
System.out.print("Enter first number: ");
double x = scanner.nextDouble();
System.out.print("Enter operator (+, -, *, /): ");
String op = scanner.next();
System.out.print("Enter second number: ");
double y = scanner.nextDouble();
BiFunction<Double, Double, Double> operation = operations.get(op);
if (operation != null) {
try {
double result = operation.apply(x, y);
System.out.println("Result: " + result);
} catch (ArithmeticException ex) {
System.out.println("Error: " + ex.getMessage());
}
} else {
System.out.println("Unsupported operation: " + op);
}
scanner.close();
}
// Method to use as a method reference
public static double multiply(double a, double b) {
return a * b;
}
}
BiFunction<Double, Double, Double>
is used to represent operations that take two numbers and return a result.Map
, allowing selection based on user input.LambdaCalculator::multiply
) is used for multiplication.This example shows how lambdas and functional interfaces can simplify even classic programming tasks like building a calculator.