Functional Programming in Java
IntermediateJava 8+ supports functional programming via lambdas, functional interfaces, and the java.util.function package — enabling composable, testable code.
Overview
Functional programming in Java centres on treating functions as first-class values: passing them as arguments, returning them, and composing them. The java.util.function package provides standard functional interfaces: Function<T,R>, Predicate<T>, Consumer<T>, Supplier<T>, BiFunction<T,U,R>, and more. Key concepts: pure functions (no side effects), immutability, function composition (compose, andThen), and currying/partial application.
Core Functional Interfaces
java.util.function provides the building blocks. Function<T,R>: takes T, returns R. Predicate<T>: takes T, returns boolean. Consumer<T>: takes T, returns nothing. Supplier<T>: takes nothing, returns T. Operator variants (UnaryOperator, BinaryOperator) for same input/output types.
import java.util.function.*;
// Function<T, R> — transformation
Function<String, Integer> length = String::length;
Function<Integer, String> toStr = Object::toString;
// compose: g.compose(f) = g(f(x))
Function<String, String> lengthStr = toStr.compose(length);
lengthStr.apply("hello"); // "5"
// andThen: f.andThen(g) = g(f(x))
Function<String, String> lengthStr2 = length.andThen(toStr);
lengthStr2.apply("hello"); // "5" (same result, different composition order)
// Predicate<T> — test
Predicate<String> isLong = s -> s.length() > 5;
Predicate<String> isUpper = s -> s.equals(s.toUpperCase());
Predicate<String> isLongAndUpper = isLong.and(isUpper);
Predicate<String> either = isLong.or(isUpper);
Predicate<String> notLong = isLong.negate();
// Consumer<T> — side effect
Consumer<String> print = System.out::println;
Consumer<String> log = s -> logger.info(s);
Consumer<String> printAndLog = print.andThen(log);
// Supplier<T> — lazy value
Supplier<List<String>> newList = ArrayList::new;
Supplier<Instant> now = Instant::now; // evaluated lazilyHigher-Order Functions and Currying
A higher-order function takes a function as a parameter or returns a function. This enables powerful abstractions: decorators, retry logic, caching wrappers, and pipeline builders.
Currying converts a multi-argument function into a chain of single-argument functions. Partial application fixes some arguments, returning a function that takes the rest.
// Higher-order function — accepts a function
public static <T, R> List<R> map(List<T> list, Function<T, R> fn) {
return list.stream().map(fn).collect(Collectors.toList());
}
List<Integer> lengths = map(List.of("a", "bb", "ccc"), String::length);
// Returns a function — timing decorator
public static <T, R> Function<T, R> timed(Function<T, R> fn, String name) {
return input -> {
long start = System.nanoTime();
R result = fn.apply(input);
long ms = (System.nanoTime() - start) / 1_000_000;
System.out.println(name + " took " + ms + "ms");
return result;
};
}
Function<String, Integer> timedLength = timed(String::length, "length");
// Currying — Function<A, Function<B, C>>
Function<Integer, Function<Integer, Integer>> add =
a -> b -> a + b;
Function<Integer, Integer> add5 = add.apply(5); // partial application
add5.apply(3); // 8
add5.apply(10); // 15
// Retry higher-order function
public static <T> Supplier<T> withRetry(Supplier<T> op, int maxAttempts) {
return () -> {
for (int i = 0; i < maxAttempts; i++) {
try { return op.get(); }
catch (Exception e) {
if (i == maxAttempts - 1) throw e;
}
}
throw new RuntimeException("unreachable");
};
}Pure Functions and Functional Style
A pure function always returns the same output for the same input and has no side effects. Pure functions are: easy to test (no mocking needed), safe to cache (memoization), and safe to run in parallel.
Functional style in Java: prefer immutable data, express transformations as stream pipelines, use Optional instead of null, and separate pure logic from I/O.
// IMPURE — depends on external state, has side effects
private List<String> cache = new ArrayList<>();
public String processImpure(String input) {
cache.add(input); // side effect — modifies state
return input + System.currentTimeMillis(); // non-deterministic
}
// PURE — same input always gives same output, no side effects
public static String processPure(String input, String suffix) {
return input.strip().toLowerCase() + suffix;
}
// Memoization — safe because the function is pure
Map<String, Integer> memo = new ConcurrentHashMap<>();
Function<String, Integer> memoizedLength =
s -> memo.computeIfAbsent(s, String::length);
// Functional pipeline — transformations as a pipeline of pure functions
List<String> result = rawData.stream()
.filter(s -> !s.isBlank()) // pure predicate
.map(String::trim) // pure transform
.map(String::toLowerCase) // pure transform
.distinct() // stateful but within stream
.sorted() // pure compare
.collect(Collectors.toUnmodifiableList()); // immutable resultKey Points to Remember
- Function<T,R>, Predicate<T>, Consumer<T>, Supplier<T> are the core functional interfaces.
- compose() applies right-to-left; andThen() applies left-to-right.
- Higher-order functions take or return functions — enables decorators, retry, timing wrappers.
- Currying/partial application: fix some arguments, return a function for the rest.
- Pure functions (no side effects, deterministic) are easy to test, cache, and parallelise.
Practice Functional Programming in Java in the Playground
Run and modify code directly in your browser - no setup needed.
Interview Questions
Sign in to ask AriaWhat is the difference between Function.compose() and Function.andThen()?
What is a pure function and why is it beneficial?
What is currying and how do you implement it in Java?
What is the difference between Consumer and Function?
How would you implement a memoization wrapper as a higher-order function?
Ask Aria about Functional Programming in Java
Your personal AI tutor — ask anything about this concept