☕ Java

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 higher-order function is defined purely by its parameter and return types, not by what it computes: it is higher-order if at least one of its parameters is itself a function, or if its return type is itself a function, or both. Everything else — what a function does once it has another function in hand — is incidental to the definition. This sits in contrast to a first-order function, whose parameters and return value are all plain data: numbers, strings, collections, and ordinary objects, none of which are themselves invokable behavior. Java methods, strictly speaking, are not values — a method cannot be assigned to a variable, passed by its own name, or returned on its own. What Java passes around instead, when writing higher-order code, is an object that implements a functional interface, with that object's single method holding the behavior the higher-order function will invoke. Practically, this distinction rarely matters once lambda expressions are involved, since the lambda syntax makes supplying that object feel exactly like supplying the function itself — but it explains why a "function" in Java always has a declared functional-interface type, rather than existing as a type-less function value the way it would in JavaScript or Haskell. Higher-order functions are not a Java 8 invention — sorting a List with a Comparator has accepted a function (wrapped in the Comparator interface) as a parameter since Java's first release, making it technically a higher-order function decades before the term entered common use in Java codebases. What changed with lambda expressions is not the capability but its ergonomics: writing a custom higher-order method and asking callers to supply a multi-line anonymous class at each call site was a real cost that discouraged the pattern outside of a few sanctioned, built-in cases; once a caller can supply that behavior as a single-line lambda, defining and using application-specific higher-order functions becomes practical for everyday code, not just for a handful of JDK APIs.
Java
// ── 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

The most common shape of higher-order function accepts a piece of behavior and applies it to data the function already has or receives separately — the function provides the structure (how to iterate, when to apply the behavior, what to do with the result) while the caller supplies the specific behavior through the parameter. The Stream API is the most visible source of these in everyday Java: map takes a Function describing how to transform each element, filter takes a Predicate describing which elements to keep, and reduce takes a BinaryOperator describing how to combine elements pairwise into a single result — the looping, short-circuiting, and accumulation logic live inside the Stream implementation, while the caller-supplied function supplies only the per-element decision. The same pattern is useful well beyond the Stream API for application-specific code: a retry utility can accept a Supplier representing the operation to retry, looping and catching exceptions itself while remaining completely agnostic about what operation it's retrying; a validation pipeline can accept a list of Predicate conditions and report which ones an object fails, without the validation logic needing to know anything about the specific business rules being checked; sorting on multiple criteria is achieved by chaining several Comparator instances with thenComparing, each one supplying the tie-breaking rule for the level above it.
Java
// ── 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

The complementary shape of higher-order function builds and returns a new function rather than (or in addition to) accepting one — typically a factory method that takes some configuration data as ordinary parameters and produces a Function, Predicate, or other functional-interface instance tailored to that configuration, ready to be applied later, possibly many times, possibly far from where it was created. This shape becomes especially useful once it's combined with the JDK's built-in composition methods: Function provides andThen and compose, letting two functions be chained into one — calling andThen on f with g produces a new function equivalent to first applying f, then applying g to f's result, while calling compose on f with g produces a new function equivalent to first applying g, then applying f to g's result, the two differing only in which function runs first. Predicate offers the logical analogues and, or, and negate, letting conditions be combined the same way conditions are combined with && and ||, but as reusable, storable values rather than inline boolean expressions. Combining "returns a function" with "composes functions" is what allows flexible pipelines to be assembled from small, independently testable pieces and then handed off as a single unit — a pattern that becomes especially powerful once paired with currying, covered in its own entry, where a function returning a function is used specifically to supply one argument at a time.
Java
// ── 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"

Related Topics in Functional Programming

Functional Programming Basics
Functional programming is a programming paradigm that treats computation as the evaluation and composition of functions, favors expressions that produce values over statements that mutate state, and prefers passing behavior around as data rather than encoding it only inside class hierarchies. Java was designed from the outset as an object-oriented, imperative language: methods could be invoked but never treated as values — they could not be stored in a variable, passed as an argument, or returned from another method, except through the verbose workaround of wrapping a single method inside an anonymous class that implemented a one-method interface. Java 8 closed this gap by introducing lambda expressions, method references, and a standard library of single-method functional interfaces in java.util.function, together with the Stream API built on top of them, giving Java a genuinely functional layer on top of its existing object-oriented core without abandoning that core. This entry covers what distinguishes functional style from imperative and object-oriented style, how Java represents a function as an instance of a functional-interface type rather than as a true first-class value, the standard functional interfaces the JDK provides for common shapes of behavior, and how adopting functional style changes the shape of everyday Java code.
Pure Functions
A pure function is a function whose return value depends solely on the arguments passed to it, with no dependence on any hidden or external state, and which produces no observable effect on the world beyond computing and returning that value — no mutation of arguments, no writes to shared state, no I/O, and no dependence on anything that can change between calls such as the system clock or a random number generator. Calling a pure function with the same arguments always produces the same result, a property known as referential transparency, which means a call to a pure function can be replaced anywhere in a program by its result without changing what the program does. Java does not enforce purity at the language level — any method, lambda, or method reference may freely read and write shared mutable state, perform I/O, or behave non-deterministically — so purity in Java is a discipline the developer chooses to follow rather than a guarantee the compiler provides. This entry covers the precise definition of purity and referential transparency, the common ways Java code becomes impure even unintentionally, and the practical payoffs of writing pure functions: simpler testing, safe caching and memoization, and safe parallel execution.
Closures
A closure is a function bundled together with the variables from its enclosing lexical scope that it refers to, captured at the time the function is created, so that the function can keep using those variables even after the scope that originally held them has finished executing. In Java, both lambda expressions and anonymous inner classes form closures: they can refer to local variables and parameters of the method that creates them, and to the fields of the enclosing object, even after that method has returned and its stack frame is gone. Java places one significant restriction on this that languages like JavaScript do not: any local variable or parameter captured by a lambda or inner class must be effectively final — assigned exactly once and never reassigned — which rules out the kind of closure-over-a-mutable-counter pattern that is idiomatic in other languages, and requires a small workaround whenever genuinely mutable shared state is needed. This entry covers what a closure captures and how, why Java enforces the effectively-final rule, the standard workarounds for needing mutable captured state, and the practical patterns and memory implications of using closures in real code.
Currying
Currying is the technique of transforming a function that accepts multiple arguments into a chain of functions that each accept exactly one argument, so that calling the first function with the first argument returns a new function waiting for the second argument, and so on until every argument has been supplied and a final result is produced — a two-argument function becomes a one-argument function that, given the first argument, returns another one-argument function that, given the second, produces the result. This is distinct from partial application, which means fixing some subset of a function's arguments to produce a new function over the rest, since partial application does not require restructuring a function into a one-argument-at-a-time chain and can be done directly on an ordinary multi-argument function. Languages such as Haskell curry every function by default, with no special syntax needed to call a curried function one argument at a time; Java has no built-in support for currying, so a curried function must be constructed by hand as a chain of nested functional interfaces, most often Function<A, Function<B, R>> for two arguments, nested one layer deeper for each additional argument. This entry covers the precise definition of currying and how it differs from partial application, how to build curried functions in Java using nested Function types and a reusable curry helper, and the practical situations where currying earns its verbosity in Java versus situations where a simpler approach reads better.