Function, Predicate, Supplier and Consumer

Know the four core interfaces in java.util.function, how to compose them, and which variant to pick for two arguments or primitive values.

SyntaxFunction<T, R> f = t -> r; // R apply(T) Predicate<T> p = t -> true; // boolean test(T) Supplier<T> s = () -> t; // T get() Consumer<T> c = t -> {}; // void accept(T)

The java.util.function package holds the functional interfaces the JDK itself uses, so your code, the collections API, streams and frameworks all agree on the same types. Four of them cover most needs:

InterfaceMethodShapeTypical use
Function<T, R>R apply(T t)T in, R outmap an entity to a DTO
Predicate<T>boolean test(T t)T in, yes or novalidation, filtering
Supplier<T>T get()nothing in, T outlazy defaults, factories
Consumer<T>void accept(T t)T in, nothing outlogging, sending, saving

The variants

Two inputs: BiFunction<T, U, R>, BiPredicate, BiConsumer (what Map.forEach takes). Same type in and out: UnaryOperator<T> and BinaryOperator<T>. For primitives there are versions such as IntPredicate, IntUnaryOperator, ToIntFunction<T> and LongSupplier. They avoid boxing every int into an Integer, which matters in hot loops over large data.

Composing

The interfaces carry default methods for building bigger behaviour from small parts. f.andThen(g) runs f then g; f.compose(g) runs g first. Predicates combine with and, or and negate, and Predicate.not(String::isBlank) (Java 11) negates a method reference. Consumer.andThen chains side effects in order. Function.identity() returns its input unchanged.

Why Supplier matters

A Supplier delays work until someone asks for the value. That is how Optional.orElseGet, Map.computeIfAbsent (which takes a Function) and Logger.log(Level, Supplier<String>) avoid doing expensive work, like a database call or building a long log message, when the result is not needed.

Mistakes people make

  • Mixing up andThen and compose. Read andThen left to right. If you find yourself reaching for compose, swapping the two functions and using andThen is often clearer.
  • Boxing by accident. Function<Integer, Integer> in a tight loop creates objects for every call outside the small cached range. Use IntUnaryOperator.
  • Short-circuiting assumptions. a.and(b) does skip b when a is false, just like &&, so put the cheap or null-guarding check first.
  • Using the generic types where a name would help. A public API that takes BiFunction<String, Integer, Map<String, List<Long>>> tells the caller nothing; a small named interface does. See the next lesson.

Example

Try it yourself
Loading editor…
Press Run to execute the code.

When to use it

  • Build a request validator from small Predicate rules combined with and(), each one unit tested on its own.
  • Normalise incoming fields with a Function chain such as strip then toLowerCase before looking up an account by email.
  • Pass a Supplier to a cache helper so the database query only runs on a cache miss.

More examples

andThen versus compose

andThen runs the receiver first and compose runs the argument first. IntUnaryOperator offers the same composition without wrapping numbers in Integer objects.

Example · java
void main() {
    Function<Integer, Integer> addOne = x -> x + 1;
    Function<Integer, Integer> timesTen = x -> x * 10;

    IO.println(addOne.andThen(timesTen).apply(2)); // (2 + 1) * 10
    IO.println(addOne.compose(timesTen).apply(2)); // (2 * 10) + 1

    // The primitive version avoids boxing each int.
    IntUnaryOperator addOneInt = x -> x + 1;
    IO.println(addOneInt.andThen(x -> x * 10).applyAsInt(2));
}
// Output:
// 30
// 21
// 30

Predicate.not, negate and BiConsumer

negate() flips a predicate you hold, while Predicate.not() flips a method reference inline. Map.forEach takes a BiConsumer, so each entry arrives as two arguments.

Example · java
void main() {
    Predicate<String> internal = path -> path.startsWith("/internal/");
    Predicate<String> isPublic = internal.negate();

    List<String> paths = new ArrayList<>(List.of("/orders", "/internal/metrics", "", "/users"));
    paths.removeIf(String::isBlank);
    for (String path : paths) {
        IO.println(path + " public=" + isPublic.test(path));
    }

    List<String> headers = List.of("Accept", " ", "Authorization");
    IO.println(headers.stream().filter(Predicate.not(String::isBlank)).toList());

    Map<String, Integer> hits = new TreeMap<>(Map.of("/users", 7, "/orders", 12));
    BiConsumer<String, Integer> report = (path, count) -> IO.println(path + " " + count);
    hits.forEach(report);
}
// Output:
// /orders public=true
// /internal/metrics public=false
// /users public=true
// [Accept, Authorization]
// /orders 12
// /users 7

Supplier: only do the work on a cache miss

The caller describes how to load the value but the helper decides whether to run it. The second call hits the cache, so the database is never queried again.

Example · java
String cached(Map<String, String> cache, String key, Supplier<String> loader) {
    String value = cache.get(key);
    if (value != null) {
        return value;
    }
    value = loader.get(); // only reached on a miss
    cache.put(key, value);
    return value;
}

String loadFromDatabase(String key) {
    IO.println("querying database for " + key);
    return "value-of-" + key;
}

void main() {
    Map<String, String> cache = new HashMap<>();
    IO.println(cached(cache, "plan:42", () -> loadFromDatabase("plan:42")));
    IO.println(cached(cache, "plan:42", () -> loadFromDatabase("plan:42")));
}
// Output:
// querying database for plan:42
// value-of-plan:42
// value-of-plan:42

Discussion

  • Be the first to comment on this lesson.