java.util.function Package
Master java.util.function: Predicate, Function, Supplier, Consumer, and their binary/triple variants for functional-style Java.
Master java.util.function: Predicate, Function, Supplier, Consumer, and their binary/triple variants for functional-style Java.
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
| Interface | Signature | Use Case |
|---|---|---|
Predicate<T> | T → boolean | Filter conditions, validation rules |
Function<T,R> | T → R | Transform one type to another |
Supplier<T> | () → T | Lazy value production, factory methods |
Consumer<T> | T → void | Side-effect operations, forEach |
BinaryOperator<T> | (T, T) → T | Combining two values of same type |
UnaryOperator<T> | T → T | In-place transformation, identity operations |
BiPredicate<T,U> | (T, U) → boolean | Two-argument filter conditions |
BiFunction<T,U,R> | (T, U) → R | Two-argument transformations |
BiConsumer<T,U> | (T, U) → void | Two-argument side effects |
When NOT to Use
- Long-running IO operations: Functional interfaces are synchronous by design. Use
CompletableFutureor 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
Predicateused 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
A Predicate<T> takes an argument of type T and returns a boolean. Think of it as a yes-or-no question about a value. You’ll see it most often in Stream.filter(predicate), where it decides which elements pass through and which get dropped. But Predicate shows up elsewhere in the JDK too: List.removeIf(Predicate), Optional.filter(Predicate), Collection.removeIf(Predicate) in Java 8+. If you’ve ever written if (someCondition(x)) as a gate before an operation, a Predicate is probably what you actually need.
Predicate gives you three composition methods. predicate.and(other) chains with AND, predicate.or(other) chains with OR, and predicate.negate() inverts the result. Predicate.isEqual(target) creates a predicate that uses Objects.equals() instead of ==, which comes in handy when you want to check if a stream element matches a specific value. These let you assemble complex conditions from simple parts instead of writing nested if statements.
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
A Function<T, R> converts an input of type T into an output of type R. It is the workhorse of java.util.function, showing up in map(Function) in streams, Optional.map(Function), and as the parent of BiFunction. Typical examples: String::toUpperCase (String to String), User::getEmail (User to String), s -> s.length() (String to Integer). Functions are how you express data transformations declaratively.
andThen and compose let you chain functions. f.andThen(g) applies f first, then g to the result, so f.andThen(g).apply(x) equals g(f(x)). compose reverses this: f.compose(g) applies g first, then f, giving f(g(x)). Use andThen when each step feeds its output into the next. Function.identity() returns its input unchanged, useful as a passthrough in collectors and mapping pipelines.
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
A Supplier<T> produces a value of type T on demand, taking no arguments. The real reason to use one is deferred computation: instead of creating an expensive default upfront and paying the cost even when you don’t need it, you pass a Supplier that only computes when get() is called. This is what makes lazy initialization work, and why Optional.orElseGet(Supplier) exists alongside the eager Optional.orElse(value).
Suppliers show up in factory patterns — they represent the ability to create objects without coupling to how they’re made. () -> new Connection() is a Supplier<Connection>. Method references like MyService::createClient are the idiomatic way to write this. Note that Supplier does not memoize — every call to get() runs the supplier body fresh. For caching, use a lazy initialization holder or a memoizing wrapper. BooleanSupplier has getAsBoolean() instead of get() to avoid boxing.
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
A Consumer<T> takes a T and returns nothing — it performs a side effect. The classic use is Stream.forEach(Consumer), where the consumer receives each element and does something with it: print it, log it, add it to a collection. Because consumers return void, you cannot chain them the way you chain Function. Instead, andThen runs two consumers sequentially on the same input, which is useful for composing side effects like logging followed by state updates.
BiConsumer<T, U> takes two arguments and performs a side effect. You’ll see it in Map.forEach((key, value) -> ...) or List.replaceAll(UnaryOperator). The Obj* consumers — ObjIntConsumer, ObjLongConsumer, ObjDoubleConsumer — accept one object and one primitive, which means no boxing when you need to work with both. Use consumers when you’re doing something that produces no return value: logging, updating UI state, sending data to an external system.
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
BinaryOperator<T> is a BiFunction<T, T, T> — both inputs and the output are the same type T. It models operations that combine two T values into one: addition, string concatenation, list merging, finding max or min. The stream.reduce(identity, BinaryOperator) pattern uses a BinaryOperator to accumulate elements. BinaryOperator.minBy(Comparator) and BinaryOperator.maxBy(Comparator) are factory methods for finding extremes according to a comparator.
UnaryOperator<T> is a Function<T, T> — input and output types are identical. Use it when a transformation keeps the same type. List.replaceAll(UnaryOperator) applies a unary operator to every element in a list, mutating it in place. The difference between UnaryOperator and Function<T, T> is purely about clarity — UnaryOperator signals that the transformation preserves type, which makes APIs easier to understand.
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
The generic functional interfaces (Predicate<T>, Function<T, R>, Supplier<T>, Consumer<T>) work with object references, so primitive values get boxed into wrapper types on every call. For small datasets this does not matter much, but in tight loops or long stream pipelines processing millions of primitives, the boxing overhead becomes noticeable. The primitive specializations (IntPredicate, IntFunction<R>, IntSupplier, IntConsumer, and their Long and Double counterparts) exist so you can stay in primitives throughout the pipeline without the conversion overhead.
IntPredicate takes a primitive int and returns boolean. ToIntFunction<T> takes any reference type and returns an int without boxing. IntFunction<R> takes a primitive int and returns a reference type R. When working with primitive streams like IntStream, LongStream, or DoubleStream, these specializations keep the pipeline in primitives, avoiding the boxing that would otherwise happen on every map or filter call. This matters most when processing large datasets where the overhead compounds.
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
| Scenario | Problem | Solution |
|---|---|---|
| Stateful predicate in parallel stream | Non-deterministic results | Use stateless, pure predicates |
| Checked exception in lambda | Lambda cannot throw checked exceptions | Wrap in unchecked exception or use a wrapper utility |
Function.identity() in map with null value | NPE if the function returns null | Use explicit (s) -> s instead of Function.identity() |
Consumer.andThen when first throws | Second consumer never runs | Log and handle errors before chaining |
IntFunction returning null for non-primitive result | Boxing ambiguity | Use Function<Integer, R> if null is a valid return |
Trade-off Table
| Aspect | Custom Interface | java.util.function |
|---|---|---|
| Reusability | May be scattered across codebase | Standardized, predictable semantics |
| API familiarity | Requires documentation | Self-documenting by type name |
| Compatibility with Streams | Requires adapter | Native compatibility |
| Naming clarity | Domain-specific names | Generic 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
Consumerfor 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
Consumer.andThenexecutes 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.Function.composeorder:f.compose(g).apply(x)appliesfto the result ofg(x)— the composed function applies right-to-left.f.andThen(g)applies left-to-right.- Boxing in stream map:
stream.map(Integer::sum)wheresumisBinaryOperator<Integer>causes boxing. UsemapToIntandsum()for primitive optimizations. Predicate.isEqualusesObjects.equalsnot==: Two differentStringinstances with the same content are considered equal byPredicate.isEqual— this may or may not be the intended behavior.Supplieris evaluated once per.get()call: Each call toget()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>extendsBiFunction<T,T,T>— both inputs and output are T.UnaryOperator<T>extendsFunction<T,T>— output is same type as input.- Use primitive specializations (
IntFunction,IntPredicate, etc.) to avoid boxing. andThenchains in execution order;composeapplies right-to-left.
Interview Questions
Further Reading
- Oracle: java.util.function package documentation — official Javadoc for all functional interfaces
- Baeldung: Java Lambda Expressions and Functional Interfaces — lambda syntax and functional interface patterns
- Functional programming in Java 8 — Oracle’s overview of functional programming with
java.util.function - Stream API and lambda performance — benchmarking study on lambda overhead
- Vavr: Functional data structures — richer functional types complementing
java.util.function - Lambda Expressions — lambdas as the calling syntax for functional interfaces
- Method References — companion to lambda expressions
- Stream API — streams heavily use java.util.function interfaces
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.functioninterfaces instead of custom single-method interfaces Predicatefor conditions,Functionfor transformations,Consumerfor side effects,Supplierfor lazy evaluationandThenchains in execution order;composeapplies 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.
Arithmetic Operators in Java
Master Java arithmetic operators: addition, subtraction, multiplication, division, and modulo with integer division gotchas and operator precedence explained.
Array Basics in Java
Learn Java array fundamentals: declaration, initialization, element access, and the length property explained simply.