Higher Order Functions
A higher-order function is a function that takes one or more other functions as parameters, returns a function as its result, or both — as opposed to a first-order function, which only takes and returns plain data values like numbers, strings, or objects. Java has supported a narrow form of this since its very first versions, since passing a Runnable to a Thread or a Comparator to Collections.sort is technically passing a function (wrapped in a single-method interface) into another method, but writing an anonymous class at every call site made the pattern feel like a special case reserved for a handful of built-in APIs rather than a general technique available for everyday code. Lambda expressions and method references, introduced in Java 8, made supplying a function as an argument concise enough that higher-order functions became a natural, everyday tool — most visibly through the Stream API's map, filter, and reduce, but equally usable in ordinary application code through java.util.function. This entry covers the definition of a higher-order function, how Java represents functions as arguments and return values without true first-class functions, the most common shapes of functions that take functions as parameters, and the complementary shape of functions that build and return other functions, including function composition.
Definition: Functions That Operate on Functions
// ── A first-order function: every parameter and the return type are data
static int sum(int a, int b) {
return a + b;
}
// ── A higher-order function: takes a FUNCTION as a parameter ───────────
static int applyTwice(int input, IntUnaryOperator operation) {
return operation.applyAsInt(operation.applyAsInt(input));
}
System.out.println(applyTwice(3, x -> x * 2)); // ((3*2)*2) = 12
System.out.println(applyTwice(3, x -> x + 10)); // ((3+10)+10) = 23
// ── Collections.sort has ALWAYS been higher-order, even pre-Java-8 ─────
List<String> names = new ArrayList<>(List.of("Mercury", "Gaia", "Io"));
Collections.sort(names, new Comparator<String>() { // pre-Java-8 style:
public int compare(String a, String b) { return a.length() - b.length(); }
});
// vs. the same higher-order call, made ergonomic by a lambda:
Collections.sort(names, (a, b) -> a.length() - b.length());Functions That Take Functions as Parameters
// ── Stream operations are higher-order functions you call constantly ───
List<String> words = List.of("functional", "imperative", "pure", "lazy", "object");
List<Integer> lengths = words.stream()
.map(String::length) // Function<String, Integer> parameter
.toList();
List<String> longWords = words.stream()
.filter(w -> w.length() > 4) // Predicate<String> parameter
.toList();
int totalLength = words.stream()
.mapToInt(String::length)
.reduce(0, Integer::sum); // BinaryOperator<Integer> parameter
// ── A custom higher-order function: retry an operation a fixed number of times
static <T> T retry(int attempts, Supplier<T> operation) {
RuntimeException lastFailure = null;
for (int i = 0; i < attempts; i++) {
try {
return operation.get(); // the caller's behavior, applied here
} catch (RuntimeException e) {
lastFailure = e;
}
}
throw lastFailure;
}
String result = retry(3, () -> callFlakyService()); // caller supplies the "what"
// ── A custom higher-order function: validate against a list of rules ───
static <T> List<String> validate(T value, List<Predicate<T>> rules, List<String> messages) {
List<String> failures = new ArrayList<>();
for (int i = 0; i < rules.size(); i++) {
if (!rules.get(i).test(value)) { // each rule is a Predicate<T> parameter
failures.add(messages.get(i));
}
}
return failures;
}
List<String> errors = validate("ab",
List.of(s -> s.length() >= 3, s -> s.matches("[a-z]+")),
List.of("too short", "must be lowercase letters"));
// errors == ["too short"]
// ── Comparator chaining: each thenComparing() call takes a function ────
record Employee(String name, String department, int salary) {}
List<Employee> employees = List.of(
new Employee("Sam", "Eng", 90000),
new Employee("Lee", "Eng", 90000),
new Employee("Pat", "Sales", 75000));
List<Employee> sorted = employees.stream()
.sorted(Comparator.comparing(Employee::department)
.thenComparing(Employee::salary, Comparator.reverseOrder())
.thenComparing(Employee::name))
.toList();Functions That Return Functions, and Function Composition
// ── A factory function that returns a configured Function ──────────────
static IntUnaryOperator multiplier(int factor) {
return x -> x * factor; // returns a NEW function, closing over the factor
}
IntUnaryOperator triple = multiplier(3);
System.out.println(triple.applyAsInt(7)); // 21
// ── A factory function that returns a configured Predicate ─────────────
static Predicate<String> hasMinLength(int min) {
return s -> s.length() >= min;
}
Predicate<String> atLeastFive = hasMinLength(5);
System.out.println(atLeastFive.test("functional")); // true
// ── Function composition: andThen() vs compose() ────────────────────────
Function<Integer, Integer> addTen = x -> x + 10;
Function<Integer, Integer> doubleIt = x -> x * 2;
Function<Integer, Integer> addThenDouble = addTen.andThen(doubleIt);
// addThenDouble(5) = doubleIt(addTen(5)) = doubleIt(15) = 30
System.out.println(addThenDouble.apply(5)); // 30
Function<Integer, Integer> doubleThenAdd = addTen.compose(doubleIt);
// doubleThenAdd(5) = addTen(doubleIt(5)) = addTen(10) = 20
System.out.println(doubleThenAdd.apply(5)); // 20
// ── Predicate composition: and / or / negate ────────────────────────────
Predicate<String> isLong = s -> s.length() > 8;
Predicate<String> hasSpace = s -> s.contains(" ");
Predicate<String> isLongPhrase = isLong.and(hasSpace);
Predicate<String> isShortWord = isLong.negate().and(hasSpace.negate());
// ── Building a small pipeline by combining returned + composed functions
static Function<String, String> pipeline(int truncateAt) {
Function<String, String> trim = String::trim;
Function<String, String> upper = String::toUpperCase;
Function<String, String> cut = s -> s.length() > truncateAt ? s.substring(0, truncateAt) : s;
return trim.andThen(upper).andThen(cut); // built once, applied many times
}
Function<String, String> clean = pipeline(5);
System.out.println(clean.apply(" functional ")); // "FUNCT"