Higher-Order Functions in Java: Closures and Composition
Java methods can take and return functions: higher-order methods, closure factories, composition with andThen, lambda strategies and lazy Suppliers.
- Course: Java study plan
- Module: Nested classes & lambdas
- Kind: Lesson
- Reading time: 13 min
- Runtime: OpenJDK 18
What is a higher-order function in Java?
A higher-order function in Java is a method that takes a function as a parameter or returns one, typed with a functional interface such as Predicate, Function or Supplier. filter(list, predicate) and Comparator.comparing(Person::age) take functions; adder(10), which returns x -> x + 10, returns one — a closure that remembers its argument.
Lesson
Once behaviour can be stored in a variable, passed to a method and returned from one, a set of techniques opens up that have no equivalent in purely object-oriented code: functions that build functions, pipelines assembled from small pieces, strategies chosen at run time from a map, and callbacks. This lesson is a tour of those techniques as they appear in everyday Java — nothing academic, all of it in real codebases.
Higher-order methods
A method that takes or returns a function:
static <T> List<T> filter(List<T> xs, Predicate<T> keep) {
List<T> out = new ArrayList<>();
for (T x : xs) if (keep.test(x)) out.add(x);
return out;
}
static <T, R> List<R> map(List<T> xs, Function<T, R> f) { … }
static <T> void retry(int times, Supplier<T> action) { … }
static <T> T withTiming(String label, Supplier<T> action) {
long t0 = System.nanoTime();
try { return action.get(); }
finally { System.out.println(label + ": " + (System.nanoTime() - t0) / 1_000_000 + " ms"); }
}
List<String> longNames = filter(names, n -> n.length() > 5);
Config c = withTiming("load", () -> loadConfig(path));
The "wrap an action" shape — timing, retrying, logging, running under a lock, in a transaction — is where Supplier/Runnable parameters shine: the wrapper owns the setup and teardown, the caller supplies the middle.
Functions that return functions (factories and currying)
static Predicate<String> startsWith(String prefix) {
return s -> s.startsWith(prefix); // the returned lambda captures prefix
}
static Function<Integer, Integer> adder(int n) {
return x -> x + n;
}
static Comparator<Person> byField(String field) {
return switch (field) {
case "age" -> Comparator.comparingInt(Person::age);
case "name" -> Comparator.comparing(Person::name);
default -> throw new IllegalArgumentException(field);
};
}
Predicate<String> isTemp = startsWith("tmp_");
Function<Integer, Integer> plus10 = adder(10);
A method returning a lambda that captured its arguments is a closure factory. adder(10) is partial application; a function returning a function of one argument at a time is currying (x -> y -> x + y, type Function<Integer, Function<Integer, Integer>>) — rare in Java, but you should recognise it.
Composition
Function<String, String> trim = String::trim;
Function<String, String> lower = String::toLowerCase;
Function<String, String> normalise = trim.andThen(lower); // trim first, then lower
Function<String, Integer> normalisedLength = normalise.andThen(String::length);
Predicate<String> notBlank = Predicate.not(String::isBlank);
Predicate<String> valid = notBlank.and(s -> s.length() < 100).or(s -> s.equals("*"));
Comparator<Person> order = Comparator.comparing(Person::lastName)
.thenComparing(Person::firstName)
.thenComparingInt(Person::age)
.reversed();
UnaryOperator<String> pipeline = List.<UnaryOperator<String>>of(String::trim, String::toLowerCase, s -> s.replace(' ', '_'))
.stream().reduce(UnaryOperator.identity(), (f, g) -> s -> g.apply(f.apply(s))); // fold a list of steps into one function
andThen/compose for functions, and/or/negate for predicates, thenComparing/reversed for comparators — the default methods on the functional interfaces are the composition vocabulary. Small named pieces composed at the use site read better than one large lambda.
The strategy pattern, with lambdas
Map<String, BinaryOperator<Long>> ops = Map.of(
"+", Long::sum,
"-", (a, b) -> a - b,
"*", (a, b) -> a * b,
"max", Math::max
);
long result = ops.getOrDefault(symbol, (a, b) -> { throw new IllegalArgumentException(symbol); }).apply(x, y);
Where Java 5 code declared an interface, four classes and a factory, a map of lambdas does the same in six lines. The interface still exists (BinaryOperator), the strategies are still swappable and testable — they are just not classes. Reach for named classes again when a strategy has state or configuration of its own.
Callbacks and event handling
button.onClick(e -> save());
executor.submit(() -> process(job));
CompletableFuture.supplyAsync(this::fetch).thenApply(this::parse).thenAccept(this::render);
list.removeIf(item -> item.isExpired());
map.computeIfAbsent(key, k -> new ArrayList<>()).add(value);
map.merge(word, 1, Integer::sum);
Every "call me when" API takes a functional interface. The collection APIs' removeIf, replaceAll, computeIfAbsent, merge, forEach are lambdas replacing loops that used to be five lines with an iterator.
Lazy evaluation with Supplier
void log(Level level, Supplier<String> message) {
if (isEnabled(level)) System.out.println(message.get()); // the string is built only if needed
}
log(DEBUG, () -> "state=" + expensiveDump());
T orElseGet(Supplier<? extends T> other) // Optional: computed only when empty
Passing a Supplier instead of a value defers the work until the callee decides it is needed — the difference between orElse(computeDefault()) (always computes) and orElseGet(this::computeDefault) (computes on demand).
Exceptions in functional code
Functional interfaces in java.util.function declare no checked exceptions. Options when the body throws one:
- Catch inside the lambda and handle or wrap:
s -> { try { return parse(s); } catch (IOException e) { throw new UncheckedIOException(e); } }. - Write a small helper interface
ThrowingFunction<T, R, E extends Exception>and an adapter that wraps to unchecked. - Restructure so the throwing call happens outside the lambda.
The first is the everyday answer; Module 12 covers the exception side.
Keeping it readable
- Name intermediate functions (
Predicate<String> valid = …) instead of nesting lambdas three deep. - Prefer the standard interfaces so callers recognise the shapes.
- Do not build a function pipeline when a plain method with three statements is clearer — functional style is a tool, not a goal.
- Side-effect-free lambdas compose and test easily; lambdas that mutate shared state do not.
Interview angle
- "What is a higher-order function?" One that takes or returns a function —
filter(list, predicate),Comparator.comparing. - "What is a closure in Java?" A lambda that captures variables from its enclosing scope;
adder(10)returns one. - "How would you implement the strategy pattern today?" A functional interface and lambdas (or a map of them), classes only for stateful strategies.
- "Why
orElseGetoverorElse?" The supplier is evaluated only when needed.
Key takeaways
- Methods can take functions (
filter,withTiming) and return them (startsWith(prefix),adder(n)) — closures capture their arguments. - Compose with
andThen/compose,and/or/negate,thenComparing/reversed. - Strategies, callbacks and collection operations (
removeIf,computeIfAbsent,merge) are lambdas now. Supplierdefers work; checked exceptions inside lambdas must be caught or wrapped.
Common questions
What is a closure in Java?
A closure is a lambda that captures variables from its enclosing scope. A method such as adder(int n) that returns x -> x + n hands back a closure: each call produces a function that remembers its own n. Captured locals must be effectively final.
How do you pass a function as a parameter in Java?
Declare the parameter with a functional interface type — Predicate<T>, Function<T, R>, Supplier<T> — and pass a lambda or method reference, as in filter(names, n -> n.length() > 5). Inside the method, call the interface's method: keep.test(x) or f.apply(x).
How do you compose functions in Java?
With the default methods on the functional interfaces: andThen and compose chain Functions, and, or and negate combine Predicates, and thenComparing and reversed build Comparators. trim.andThen(lower) trims first, then lowercases.
How do you implement the strategy pattern with lambdas?
Use a functional interface as the strategy type and lambdas as the strategies — often in a map, such as a Map<String, BinaryOperator<Long>> from an operator symbol to Long::sum or (a, b) -> a - b. Keep named classes for strategies that carry state or configuration of their own.
What is currying in Java?
Currying turns a function of several arguments into a chain of one-argument functions: x -> y -> x + y, typed Function<Integer, Function<Integer, Integer>>. Fixing some arguments early, as adder(10) does, is partial application. Both work in Java but are rare.
Exercises
Build a text pipeline
Read a first line containing step names separated by spaces, chosen from trim, lower, upper, snake (spaces → underscores) and reverse; then an integer n and n lines of text. Build the pipeline once as a single Function<String, String> by folding the steps with andThen (start from Function.identity()), then apply it to each line and print the result. Also write a factory static Function<String, String> step(String name) returning the lambda for a step name (throw IllegalArgumentException for an unknown name).
Example: steps trim lower snake, then 2 lines Hello World and Java Rocks →
hello_world
java_rocksA pipeline from tokens
Read an integer k and k step names, then lines of text until end of input. Build one Function<String, String> by composing the steps in order with andThen: upper (upper-case), trim, reverse, repeat2 (the text twice), exclaim (append !). Store the named steps in a Map<String, Function<String, String>>. Apply the composed function to every line and print the result.
Example: 2 then trim upper, then lines hello and ok →
HELLO
OKIn this module: Nested classes & lambdas
← Method references — the four kinds · Checkpoint — Nested classes & lambdas →