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.
Function<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:
| Interface | Method | Shape | Typical use |
|---|---|---|---|
Function<T, R> | R apply(T t) | T in, R out | map an entity to a DTO |
Predicate<T> | boolean test(T t) | T in, yes or no | validation, filtering |
Supplier<T> | T get() | nothing in, T out | lazy defaults, factories |
Consumer<T> | void accept(T t) | T in, nothing out | logging, 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
andThenandcompose. ReadandThenleft to right. If you find yourself reaching forcompose, swapping the two functions and usingandThenis often clearer. - Boxing by accident.
Function<Integer, Integer>in a tight loop creates objects for every call outside the small cached range. UseIntUnaryOperator. - Short-circuiting assumptions.
a.and(b)does skipbwhenais 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
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.
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
// 30Predicate.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.
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 7Supplier: 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.
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