skip to content

How does java.util.function realize function composition, and what do andThen, compose, Predicate.and/or/negate, and Function.identity give you?

level: middleimportance: should knowfreq 58%

answer

  1. andThen = this first (forward pipeline)
  2. compose = argument first (maths g∘f order)
  3. Function.identity() = x->x, neutral element / toMap value
  4. Predicate.and/or/negate + static Predicate.not
  5. Combinators return new lambdas; nothing runs until apply

basics

~20 s

The java.util.function package has small function types (Function, Predicate, Consumer...) with helper methods that glue functions together: andThen runs another function after this one, compose runs it before, and Predicate has and/or/negate to combine yes/no tests. This lets you build a bigger operation from small reusable ones.

solid answer

~40 s

Function composition means building a new function by chaining existing ones so the output of one feeds the next. java.util.function provides the standard combinators as default methods on its interfaces. On Function<T,R>: f.andThen(g) makes x -> g(f(x)) (f first), while f.compose(g) makes x -> f(g(x)) (g first); the static Function.identity() returns x -> x, handy as a neutral element or a Collectors.toMap key/value mapper. Predicate<T> composes with and, or, negate for boolean logic without nested ifs, e.g. isActive.and(isAdult). Consumer<T> has andThen to run two side-effects in order. These are just methods returning new lambdas, so composition is lazy in the sense that nothing runs until you apply the result. The payoff is small, named, testable building blocks you assemble declaratively instead of writing one big imperative method.

code

java · 21 lines
java
import java.util.function.Function;
import java.util.function.Predicate;

class Combinators {
    void demo() {
        Function<Integer, Integer> times2 = x -> x * 2;
        Function<Integer, Integer> plus3  = x -> x + 3;

        // andThen: times2 first, then plus3  ->  (x*2)+3
        System.out.println(times2.andThen(plus3).apply(5)); // 13
        // compose: plus3 first, then times2  ->  (x+3)*2
        System.out.println(times2.compose(plus3).apply(5)); // 16

        Predicate<String> nonBlank = s -> !s.isBlank();
        Predicate<String> shortEnough = s -> s.length() <= 5;
        Predicate<String> valid = nonBlank.and(shortEnough);
        System.out.println(valid.test("hi"));     // true
        System.out.println(valid.test("   "));    // false
        System.out.println(valid.negate().test("   ")); // true
    }
}

go deeper

for a junior

Knows the package has small function types with helper methods like andThen to chain them, and can use Predicate.and to combine two tests.

for a middle

Correctly distinguishes andThen vs compose order, uses Predicate.and/or/negate and Predicate.not, and applies Function.identity() in Collectors.toMap.

for a senior

Explains combinators return new lambdas that run only on application, treats identity() as the neutral element, and knows null-argument and exception-propagation semantics of the combinators.

for a principal

Frames these as the JDK's minimal expression of compositional/algebraic structure, weighs them against richer libraries (vavr Try/Either) for error handling, and guides when composing tiny functions helps versus hurts readability.

## What 'function composition' means Given two functions f and g, **composition** is making a new function h such that h(x) = g(f(x)) — you feed x to f, then feed f's result to g. Mathematically this is written g ∘ f ('g after f'). Composition lets you build complex transformations out of tiny, independently-understandable ones. ## The java.util.function toolbox `java.util.function` is the standard library package of general-purpose **functional interfaces** (each an interface with one abstract method, so a lambda can supply it). The core families: - **Function<T,R>** — takes a T, returns an R. Abstract method: `R apply(T)`. - **Predicate<T>** — takes a T, returns a boolean (a test). Abstract method: `boolean test(T)`. - **Consumer<T>** — takes a T, returns nothing (a side-effect). Abstract method: `void accept(T)`. - **Supplier<T>** — takes nothing, returns a T (a factory). Abstract method: `T get()`. - **UnaryOperator<T>/BinaryOperator<T>** — Function/BiFunction specialized to one type (T->T, (T,T)->T). Beyond their single abstract method, these interfaces carry **default methods** — concrete methods defined on the interface — that are the **combinators**: methods which take/return functions to combine them. ## Combinators on Function - `f.andThen(g)` returns a new function `x -> g.apply(f.apply(x))`: **f runs first**, then g. Reads left-to-right like a pipeline. - `f.compose(g)` returns `x -> f.apply(g.apply(x))`: **g runs first**, the mathematical g∘f order. - The static `Function.identity()` returns the identity function `x -> x` (returns its input unchanged). It is the **neutral element** of composition: `f.andThen(identity())` equals `f`. It is most often used as a mapper that 'keeps the value as-is', e.g. `Collectors.toMap(keyFn, Function.identity())` to map each item to itself. Mnemonic: **andThen** = forward order (this, *and then* that); **compose** = reverse order (the maths convention). ## Combinators on Predicate Predicates model yes/no tests, and their combinators are boolean logic: - `p.and(q)` — true only if both p and q are true (short-circuits like &&). - `p.or(q)` — true if either is true (short-circuits like ||). - `p.negate()` — flips the result. - static `Predicate.not(p)` (Java 11+) — negates a predicate, convenient with method references: `filter(Predicate.not(String::isBlank))`. So instead of `if (isActive(u) && isAdult(u))` you can build `Predicate<User> eligible = isActive.and(isAdult);` and pass `eligible` to a stream `filter`. ## Combinators on Consumer `c1.andThen(c2)` returns a consumer that runs c1's side-effect, then c2's, on the same input — useful to fan a value to multiple sinks (`log.andThen(audit)`). ## Why this matters Composition turns a long imperative method into a set of **small, named, reusable, individually-testable functions** that you assemble declaratively. Each combinator just *returns a new lambda* — nothing executes until you actually call `apply`/`test`/`accept` on the composed result, so building a pipeline is cheap and side-effect-free until invoked. A subtlety: `andThen`/`compose` throw `NullPointerException` if you pass a null function (they call `Objects.requireNonNull` on the argument). And composition does not magically handle errors — if `f` throws, the whole composed call throws; wrap or use a Try-style type if you need error handling inside a pipeline. From these pieces: junior = 'helper methods chain functions together'; middle = the andThen vs compose order and Predicate and/or/negate; senior = identity as neutral element, lazy-until-applied nature, null/exception semantics; principal = how these combinators express algebraic structure and where a richer FP library (vavr) adds what the JDK omits.

  • What is the difference between f.andThen(g) and f.compose(g)?
    andThen runs f first then g: x -> g(f(x)). compose runs g first then f: x -> f(g(x)), matching the mathematical g∘f. They are mirror images; pick by reading order — andThen reads left-to-right as a pipeline.
  • Where is Function.identity() actually useful?
    As the neutral element of composition, and very commonly as a mapper that keeps the element unchanged, e.g. Collectors.toMap(User::id, Function.identity()) to index a list of users by id while storing the whole user as the value.

Combinators are like snapping together plumbing pipes. Each function is a pipe segment; andThen connects this pipe's outlet to the next pipe's inlet (forward), compose attaches a pipe before this one's inlet. Predicate.and/or are T-joints that only let water through if both/either upstream valves are open. Nothing flows until you turn the tap (apply).

saying these in an interview costs you the question

  • Swapping andThen and compose execution order
  • Believing composition runs eagerly when you build it (it runs only on apply)
  • Thinking composition adds error handling — an exception in any stage propagates
  • Reimplementing identity as x -> x everywhere instead of Function.identity()

context