java.util.function Package

Master java.util.function: Predicate, Function, Supplier, Consumer, and their binary/triple variants for functional-style Java.

published: reading time: 18 min read author: Geek Workbench

Introduction

The java.util.function package (added in Java 8) provides purpose-built functional interfaces that serve as the backbone of the Stream API, lambda expressions, and method references. Rather than forcing developers to create custom single-method interfaces, the JDK ships a standardized set that covers the most common transformation patterns.

When to Use

InterfaceSignatureUse Case
Predicate<T>T → booleanFilter conditions, validation rules
Function<T,R>T → RTransform one type to another
Supplier<T>() → TLazy value production, factory methods
Consumer<T>T → voidSide-effect operations, forEach
BinaryOperator<T>(T, T) → TCombining two values of same type
UnaryOperator<T>T → TIn-place transformation, identity operations
BiPredicate<T,U>(T, U) → booleanTwo-argument filter conditions
BiFunction<T,U,R>(T, U) → RTwo-argument transformations
BiConsumer<T,U>(T, U) → voidTwo-argument side effects

When NOT to Use

  • Long-running IO operations: Functional interfaces are synchronous by design. Use CompletableFuture or reactive types (Flux, Mono) for async work.
  • Checked exceptions: The functional interfaces do not declare checked exceptions. Wrap in unchecked exceptions or use a Function<T, R> that re-throws.
  • Stateful predicates in streams: A Predicate used as a stream filter should be stateless and pure — stateful predicates produce non-deterministic results when used in parallel pipelines.

Interface Taxonomy

classDiagram
    class Predicate~T~ {
        <<@FunctionalInterface>>
        +test(T) boolean
        +and(Predicate) Predicate
        +or(Predicate) Predicate
        +negate() Predicate
        +isEqual(Object) Predicate
    }
    class Function~T,R~ {
        <<@FunctionalInterface>>
        +apply(T) R
        +andThen(Function) Function
        +compose(Function) Function
        +identity() Function
    }
    class Supplier~T~ {
        <<@FunctionalInterface>>
        +get() T
    }
    class Consumer~T~ {
        <<@FunctionalInterface>>
        +accept(T) void
        +andThen(Consumer) Consumer
    }
    class BinaryOperator~T~ {
        <<@FunctionalInterface>>
        +apply(T, T) T
        +minBy(Comparator) BinaryOperator
        +maxBy(Comparator) BinaryOperator
    }
    class UnaryOperator~T~ {
        <<@FunctionalInterface>>
        +apply(T) T
    }

    Predicate ..|> Object
    Function ..|> Object
    Supplier ..|> Object
    Consumer ..|> Object
    BinaryOperator --|> BiFunction
    UnaryOperator --|> Function

Code Examples

Predicate — Filtering and Conditions

import java.util.function.Predicate;
import java.util.List;

Predicate<String> isNonBlank = s -> !s.isBlank();
Predicate<String> hasAtSymbol = s -> s.contains("@");
Predicate<String> isValidEmail = isNonBlank.and(hasAtSymbol);

// negate
Predicate<String> isInvalidEmail = isValidEmail.negate();

// Combining with or
Predicate<String> isAdminOrUser = s -> s.startsWith("admin_")
    .or(s -> s.startsWith("user_"));

// static isEqual
Predicate<Object> isNull = Predicate.isEqual(null);
Predicate<String> isHello = Predicate.isEqual("hello");

// Using in stream filter
List<String> emails = List.of("alice@example.com", "bob", "charlie@test.com");
List<String> valid = emails.stream()
    .filter(isValidEmail)
    .toList(); // [alice@example.com, charlie@test.com]

Function — Transformations

import java.util.function.Function;
import java.util.List;

Function<String, Integer> length = String::length;
Function<Integer, String> repeat = n -> "*".repeat(n);

// andThen — apply this first, then the other
Function<String, String> label = length.andThen(repeat);

// compose — apply the other first, then this
Function<String, Integer> totalStars = repeat.compose(length.compose(repeat));

// identity
Function<String, String> identity = Function.identity();

// Chain multiple transforms
record User(Long id, String name, String department) {}
List<User> users = List.of(
    new User(1L, "Alice", "Engineering"),
    new User(2L, "Bob", "Marketing")
);
List<String> deptNames = users.stream()
    .map(u -> u.department())
    .map(String::toUpperCase)
    .distinct()
    .toList(); // [ENGINEERING, MARKETING]

Supplier — Lazy Evaluation

import java.util.function.Supplier;

// Lazy default value — only computed when absent
Supplier<Connection> connectionSupplier = () -> createExpensiveConnection();
Connection conn = Optional.ofNullable(cachedConnection)
    .orElseGet(connectionSupplier);

// Factory pattern
Supplier<LocalDate> todaySupplier = LocalDate::now;

// Singleton pattern
Supplier<List<String>> listSupplier = () -> new ArrayList<>(); // new list each time
// For constant list
Supplier<List<String>> constantList = List::of; // same list returned every time

Consumer — Side Effects

import java.util.function.Consumer;

// forEach with Consumer
Consumer<String> printer = System.out::println;
List.of("a", "b", "c").forEach(printer);

// andThen chain
Consumer<String> upperPrinter = s -> System.out.println(s.toUpperCase());
Consumer<String> withTimestamp = s -> System.out.println("[INFO] " + s);
Consumer<String> combined = upperPrinter.andThen(withTimestamp);
combined.accept("hello"); // prints HELLO then [INFO] hello

// BiConsumer
BiConsumer<String, Integer> kvPrinter = (k, v) -> System.out.println(k + "=" + v);
kvPrinter.accept("score", 42); // score=42

BinaryOperator and UnaryOperator

import java.util.function.BinaryOperator;
import java.util.Arrays;

// BinaryOperator — combine two same-typed values
BinaryOperator<Integer> sum = Integer::sum;
BinaryOperator<Integer> max = Integer::max;
BinaryOperator<Integer> min = Integer::min;

// Using with reduce
int total = Arrays.asList(1, 2, 3, 4, 5).stream()
    .reduce(0, sum); // 15

// minBy / maxBy
BinaryOperator<Integer> youngest = BinaryOperator.minBy(Comparator.comparingInt(User::getAge));
BinaryOperator<Integer> oldest = BinaryOperator.maxBy(Comparator.comparingInt(User::getAge));

// UnaryOperator — transform a value to same type
UnaryOperator<String> toTitleCase = s ->
    s.substring(0, 1).toUpperCase() + s.substring(1).toLowerCase();
UnaryOperator<String> addPrefix = s -> "ID-" + s;
UnaryOperator<String> composed = addPrefix.compose(toTitleCase);
composed.apply("alice"); // "ID-Alice"

Primitive-Focused Specializations

import java.util.function.IntPredicate;
import java.util.function.IntFunction;
import java.util.function.IntSupplier;
import java.util.function.IntConsumer;
import java.util.function.ToIntFunction;

// IntPredicate — avoids boxing
IntPredicate isEven = n -> n % 2 == 0;
boolean result = isEven.test(42); // true

// ToIntFunction — converts any type to int
ToIntFunction<String> strLength = String::length;
strLength.applyAsInt("hello"); // 5

// IntSupplier — generates primitive ints
IntSupplier randomInt = () -> new Random().nextInt(100);

// IntFunction — takes int, returns any type
IntFunction<String> repeatStars = n -> "*".repeat(n);
repeatStars.apply(3); // "***"

// IntConsumer
IntConsumer printInt = System.out::println;
IntStream.range(1, 4).forEach(printInt); // 1, 2, 3

Failure Scenarios

ScenarioProblemSolution
Stateful predicate in parallel streamNon-deterministic resultsUse stateless, pure predicates
Checked exception in lambdaLambda cannot throw checked exceptionsWrap in unchecked exception or use a wrapper utility
Function.identity() in map with null valueNPE if the function returns nullUse explicit (s) -> s instead of Function.identity()
Consumer.andThen when first throwsSecond consumer never runsLog and handle errors before chaining
IntFunction returning null for non-primitive resultBoxing ambiguityUse Function<Integer, R> if null is a valid return

Trade-off Table

AspectCustom Interfacejava.util.function
ReusabilityMay be scattered across codebaseStandardized, predictable semantics
API familiarityRequires documentationSelf-documenting by type name
Compatibility with StreamsRequires adapterNative compatibility
Naming clarityDomain-specific namesGeneric names (Predicate, Function, etc.)

Observability Checklist

// Wrapping functions for observability
public <T, R> Function<T, R> observedFunction(String name, Function<T, R> fn) {
    return t -> {
        long start = System.nanoTime();
        try {
            R result = fn.apply(t);
            System.out.println("metric=" + name + " duration_ns=" + (System.nanoTime() - start));
            return result;
        } catch (RuntimeException e) {
            System.out.println("metric=" + name + " error=true");
            throw e;
        }
    };
}

// Predicate with logging
public <T> Predicate<T> loggedPredicate(String label, Predicate<T> predicate) {
    return t -> {
        boolean result = predicate.test(t);
        System.out.println("predicate=" + label + " input=" + t + " result=" + result);
        return result;
    };
}
  • Wrap stream pipeline stages in observability proxies for latency-critical operations.
  • Use Consumer for side-effect logging in forEach — not for business logic.
  • Track predicate failure rates to identify broken validation rules.
  • Instrument Supplier.get() calls to measure lazy initialization costs.
  • Add metric tags for function names when using functional composition patterns.

Security Notes

  • Lambda capture of mutable state: Lambdas that capture and mutate external state create data races in concurrent contexts. Keep captured variables effectively immutable.
  • Deserializing lambdas: Serialized lambdas (used in distributed caches or session storage) can be a vector for code injection if the classloader is compromised. Avoid serializing lambdas from untrusted sources.
  • Predicate injection: User-supplied predicates in search or filtering APIs must be sandboxed — a malicious predicate could cause denial-of-service via exponential complexity (ReDoS).

Pitfalls

  1. Consumer.andThen executes left-to-right, not right-to-left: a.andThen(b) means “apply a, then apply b to the result of a” — this is the natural flow but easy to misread as the opposite.
  2. Function.compose order: f.compose(g).apply(x) applies f to the result of g(x) — the composed function applies right-to-left. f.andThen(g) applies left-to-right.
  3. Boxing in stream map: stream.map(Integer::sum) where sum is BinaryOperator<Integer> causes boxing. Use mapToInt and sum() for primitive optimizations.
  4. Predicate.isEqual uses Objects.equals not ==: Two different String instances with the same content are considered equal by Predicate.isEqual — this may or may not be the intended behavior.
  5. Supplier is evaluated once per .get() call: Each call to get() re-evaluates the supplier body — it is not memoized by default.

Quick Recap

  • Predicate<T>test(T) returns boolean; use for filtering and conditions.
  • Function<T,R>apply(T) returns R; use for transformations.
  • Supplier<T>get() returns T; use for lazy initialization and factories.
  • Consumer<T>accept(T) returns void; use for side effects.
  • BinaryOperator<T> extends BiFunction<T,T,T> — both inputs and output are T.
  • UnaryOperator<T> extends Function<T,T> — output is same type as input.
  • Use primitive specializations (IntFunction, IntPredicate, etc.) to avoid boxing.
  • andThen chains in execution order; compose applies right-to-left.

Interview Questions

1. What is the difference between `Function.andThen` and `Function.compose`?

Model Answer: "`andThen` creates a pipeline that executes the calling function first, then passes its result to the provided function. `compose` executes the provided function first, then passes its result to the calling function — it is right-to-left composition. For example, `f.andThen(g)` means `g(f(x))`, while `f.compose(g)` means `f(g(x))`. Use `andThen` for sequential transformations where each step builds on the previous; use `compose` when you want to pre-process input before a main transformation."

2. Why do primitive specializations like `IntPredicate` and `IntFunction` exist?

Model Answer: "The generic functional interfaces like `Predicate` and `Function` operate on object references, which means primitive values must be boxed into wrapper types (`Integer`, `Double`, etc.) on every call. Primitive specializations (`IntPredicate`, `IntFunction`, `IntSupplier`, etc.) operate directly on primitive types without boxing, eliminating the memory allocation and garbage collection overhead for primitive values. They exist purely for performance in performance-sensitive code paths like streams and collections."

3. What is the difference between `Consumer.andThen` and `Predicate.and`?

Model Answer: "`Consumer.andThen` chains two consumers to execute sequentially — the second consumer runs on the result of the first. If the first consumer throws, the second never runs. `Predicate.and` composes two predicates using logical AND — both are evaluated even if the first is false (short-circuit evaluation is not guaranteed for the combined predicate, though the JVM may optimize it). `Predicate.or` and `Predicate.negate` similarly compose boolean logic."

4. What does `Predicate.isEqual` do and how does it differ from direct equality comparison?

Model Answer: "`Predicate.isEqual(Object target)` returns a predicate that tests using `Objects.equals(o, target)` — it uses value equality (`equals()`), not reference equality (`==`). This means two different `String` instances containing the same characters will be considered equal. Use direct `==` comparison only when you need identity comparison. `isEqual` is useful when you want to match a specific object value in a collection without overriding `equals` on the matched-against object."

5. Can a functional interface throw a checked exception?

Model Answer: "No, the standard `java.util.function` interfaces do not declare checked exceptions. If your lambda body throws a checked exception, you have three options: catch and wrap in an unchecked exception (RuntimeException), use a custom functional interface that declares the exception, or use a wrapper utility that translates checked to unchecked. Libraries like Vavr provide alternative functional interfaces (`CheckedFunction1`, etc.) that preserve checked exception signatures."

6. What is `BiFunction` and when should you use it versus `Function`?

Model Answer: "`BiFunction` represents a function that takes two arguments and produces a result — `(T, U) -> R`. Use it when your transformation requires two input values. Use `Function` for single-argument transformations. Examples: `BiFunction` for `String.repeat(count)`, `Function` for `String.toLowerCase()`. `BiFunction` does not have `andThen` directly — use `BiFunction.andThen()` after Java 9."

7. What is the difference between `Supplier.get()` and a cached supplier pattern?

Model Answer: "`Supplier.get()` is called fresh on every invocation — it is not memoized. Each call to `get()` executes the supplier body. To cache the result, use `Supplier cached = Suppliers.memoize(() -> expensiveComputation())` from Guava or implement a lazy holder pattern. For lazy initialization of expensive objects, `Double-checked locking` with a `volatile` field or the initialization-on-demand holder idiom is thread-safe without synchronization on every access."

8. What does `Predicate.or()` return when combining predicates and how does it short-circuit?

Model Answer: "`predicateA.or(predicateB)` returns a new `Predicate` that evaluates to `true` if either predicate returns `true`. The second predicate is not evaluated if the first returns `true` — this is short-circuit evaluation. This matters when the second predicate has side effects or is expensive. `Predicate.and()` similarly short-circuits by skipping the second predicate when the first returns `false`. `Predicate.negate()` has no short-circuit concern."

9. What is the purpose of `ToIntFunction`, `ToLongFunction`, and `ToDoubleFunction`?

Model Answer: "These are specializations of `Function` for primitive return types — they accept any reference type and return a primitive without boxing. `ToIntFunction` applies `String::length` returning `int` without boxing overhead. Without these, you would use `Function` which boxes the `int` result. Use them in stream operations like `stream.mapToInt(ToIntFunction)` to stay in primitive streams."

10. What is the difference between `Consumer.andThen()` and chaining multiple `Consumer` calls?

Model Answer: "`a.andThen(b)` executes `a` first, then `b` on the result of `a` — the input to `b` is the same object that `a` received (since `Consumer` returns void). This is sequential chaining: both consumers see the same input. Using `a.accept(x); b.accept(x);` is equivalent but more verbose. `andThen` is the functional style for composing side effects. Note that if `a` throws, `b` never runs — `andThen` does not provide error recovery."

11. What is `UnaryOperator` and how does it differ from `Function`?

Model Answer: "`UnaryOperator` extends `Function` with `apply(T)` returning `T`. It is semantically clearer — when the input and output types are the same, `UnaryOperator` documents this intent. `Function` could theoretically transform `T` to a different type `U`, though they happen to share the same type variable. Use `UnaryOperator` for in-place transformations (e.g., `list.replaceAll(UnaryOperator)`) and `Function` for general single-type transformations."

12. What is the difference between `IntFunction` and `Function`?

Model Answer: "`IntFunction` takes a primitive `int` and returns `R` — no boxing of the input. `Function` takes the `Integer` wrapper type — boxing occurs on every call. `IntFunction` is more efficient in streams: `stream.mapToObj(IntFunction)` avoids boxing on the input side. Use primitive specializations (`IntFunction`, `IntUnaryOperator`, etc.) in performance-sensitive code paths that process primitives."

13. What is `BinaryOperator.minBy()` and `BinaryOperator.maxBy()` and how do they work?

Model Answer: "`BinaryOperator.minBy(Comparator)` returns a `BinaryOperator` that returns the lesser element according to the given comparator. `maxBy()` returns the greater. These are convenience methods for `BinaryOperator` when you need to compare two values to find min/max. They are often used with `stream.reduce(BinaryOperator.minBy(Comparator...))` for aggregating by minimum or maximum value."

14. What is the purpose of `ObjDoubleConsumer` and similar specialized consumers?

Model Answer: "`ObjDoubleConsumer` represents an operation that accepts an object of type `T` and a primitive `double` — `(T, double) -> void`. The `Obj*` variants exist for all combinations where one parameter is an object and one is a primitive. They avoid boxing when a consumer needs to work with both object and primitive types. For example, `ObjDoubleConsumer` for logging with a numeric value without boxing the double."

15. What does `Function.identity()` return and when should you use it versus `(x) -> x`?

Model Answer: "`Function.identity()` returns a function that always returns its input argument — `x -> x`. It is equivalent to `(x) -> x` but is the standard, reusable form. Use it when you need a function that passes through values unchanged, such as in collectors: `Collectors.mapping(Function.identity(), Collectors.toList())` to collect elements unchanged into a list. The lambda form `(x) -> x` is equally valid but `Function.identity()` is self-documenting."

16. What is the difference between `BiConsumer.andThen()` and `BiConsumer` chaining with two separate calls?

Model Answer: "`biConsumerA.andThen(biConsumerB)` runs `A` then `B` sequentially on the same arguments. Calling `A.accept(a, b); B.accept(a, b);` separately is equivalent. Both consumers see the same `(a, b)` input — neither transforms the input for the other. `andThen` is the functional composition idiom for side-effect operations. If `A` throws, `B` is never called."

17. What is the relationship between `BiFunction` and `Function` in terms of composition?

Model Answer: "`BiFunction` does not have its own `compose` or `andThen` methods in the same way `Function` does. However, `BiFunction.andThen(Function)` (available in Java 9+) allows composing a `BiFunction` with a `Function` to transform the result — `(T, U) -> R` followed by `R -> V` produces a `BiFunction`. This is the composition pattern for two-argument functions."

18. What is the difference between `IntPredicate` and `Predicate` in terms of performance?

Model Answer: "`IntPredicate` takes a primitive `int` and returns `boolean` — no boxing of the input. `Predicate` accepts a boxed `Integer`, boxing the input on every call. In tight loops or stream pipelines over primitive `int` values, `IntPredicate` is significantly faster due to avoiding boxing. For streams of `Integer` objects, boxing is already occurring upstream, so `IntPredicate` cannot help."

19. What does `Supplier` look like in the context of lazy initialization and how does it differ from a method reference?

Model Answer: "A `Supplier` for lazy initialization produces a value on demand — `Supplier connSupplier = () -> createConnection()`. When used with `Optional.orElseGet(connSupplier)`, the connection is only created if needed. A method reference like `MyClass::createConnection` is a cleaner form of the same thing. The key distinction is that `Supplier.get()` has no parameters; method references to static or instance methods with zero parameters can be used as `Supplier` implementations."

20. What is the relationship between `ToIntBiFunction`, `ToLongBiFunction`, and `ToDoubleBiFunction`?

Model Answer: "These are to `BiFunction` what `ToIntFunction` is to `Function` — they accept two reference-type arguments (`T, U`) and return a primitive (`int`, `long`, `double`) without boxing the return value. They exist for the same performance reasons as their single-argument counterparts. Use them when you need a two-argument transformation that produces a primitive result."

Further Reading

Conclusion

java.util.function is the standardized vocabulary of functional programming in Java 8+. Before this package, every project had its own collection of single-method interfaces for callbacks, transformations, and predicates. The JDK’s built-in set covers the vast majority of use cases: Predicate for boolean tests, Function for transformations, Supplier for lazy production, Consumer for side effects, and their binary variants. Using these standardized types makes APIs self-documenting and interoperability between libraries seamless.

The functional interfaces are not just for streams — they appear throughout the JDK and any modern Java library. Spring uses Function<T, R> for bean transformers, Jackson uses Predicate for property filters, and most reactive libraries use Function for map operations. Once you internalize these types, you will recognize them everywhere in Java ecosystems and your code will become more idiomatic by using them consistently.

Function composition with andThen and compose is one of the most powerful patterns. andThen chains operations in execution order (apply this, then apply the other), while compose applies the other function first (right-to-left). For building transformation pipelines — parsing, validating, transforming, formatting — functional composition with andThen produces readable, testable, reusable code that is easy to modify.

The primitive specializations (IntPredicate, IntFunction, IntSupplier, etc.) exist for performance — they avoid boxing primitive values into wrapper objects. In hot paths like stream operations on large datasets, boxing overhead adds up. If you find yourself writing stream.map(Integer::sum) on an IntStream, switch to mapToInt and sum() to stay in primitives.

Functional interfaces integrate deeply with java.util.stream.Stream — every stream operation consumes one — and with java.util.Optional where map and flatMap accept Function and Function returning Optional respectively.

  • Use standardized java.util.function interfaces instead of custom single-method interfaces
  • Predicate for conditions, Function for transformations, Consumer for side effects, Supplier for lazy evaluation
  • andThen chains in execution order; compose applies right-to-left
  • Use primitive specializations (IntFunction, IntPredicate) in hot paths to avoid boxing overhead
  • Functional interfaces cannot declare checked exceptions — wrap or use a custom variant for exception-heavy logic
  • Method references (String::length, User::getName) are the cleanest lambda form when they match the target signature

Category

Related Posts

Abstract Classes in Java

Learn about partially implemented classes that define contracts for subclasses using abstract methods and concrete implementations.

#java-abstract-classes #java #java-fundamentals

Arithmetic Operators in Java

Master Java arithmetic operators: addition, subtraction, multiplication, division, and modulo with integer division gotchas and operator precedence explained.

#java-arithmetic-operators #java #java-fundamentals

Array Basics in Java

Learn Java array fundamentals: declaration, initialization, element access, and the length property explained simply.

#java-array-basics #java #java-fundamentals