java.util.function Package

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

published: reading time: 21 min read author: Geek Workbench
Quick Summary

Master java.util.function: Predicate, Function, Supplier, Consumer, and their binary/triple variants for functional-style Java. The guide uses practical examples to explain when to use, when not to use and shows how to apply the ideas in a Spring Boot project. It closes with common pitfalls and production checks so you can apply the pattern with fewer surprises.

Introduction

Before Java 8, if you wanted to pass behavior around — say, a filter rule or a transformation — you wrote a custom interface:

// Pre-Java 8: every project had these lying around
interface StringProcessor {
    boolean process(String input);
}

// Usage required an anonymous class — verbose
List<String> result = filter(list, new StringProcessor() {
    public boolean process(String input) {
        return input.length() > 3;
    }
});

Java 8 gave us java.util.function — a standard library of purpose-built functional interfaces. No more custom interfaces for common patterns. The JDK ships Predicate for yes/no tests, Function for transformations, Consumer for side effects, and Supplier for lazy value production. These are the building blocks that back the Stream API, lambda expressions, and method references.

// Same filter, now using Predicate — concise and self-documenting
List<String> result = filter(list, s -> s.length() > 3);
Predicate<String> longerThanThree = s -> s.length() > 3;
List<String> filtered = list.stream()
    .filter(longerThanThree)
    .toList();

This post covers every interface in java.util.function: their signatures, composition methods, primitive specializations, and the pitfalls that trip people up in practice.

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 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

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 matching a stream element against a specific value. This lets you build complex conditions from simple parts instead of 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 shows 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. It’s 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 main use case 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 why Optional.orElseGet(Supplier) exists alongside the eager Optional.orElse(value).

Suppliers show up in factory patterns. A supplier is essentially a way to create objects without tying yourself to how they’re instantiated. () -> 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 again. 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 — 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. This 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 stylistic. UnaryOperator makes it obvious that the transformation preserves type, which makes APIs easier to read.

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 doesn’t matter much, but in tight loops or long stream pipelines processing millions of primitives, the boxing overhead adds up. 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 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.

Interview Questions

1. What is the difference between Function.andThen and Function.compose?
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?
The generic functional interfaces like Predicate<T> and Function<T,R> operate on object references, which means primitive values must be boxed into wrapper types (Integer, Double, etc.) on every call. Primitive specializations (IntPredicate, IntFunction<R>, 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?
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?
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?
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?
BiFunction<T, U, R> 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<T, R> for single-argument transformations. Examples: BiFunction<String, Integer, String> for String.repeat(count), Function<String, String> 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?
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<Double> 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?
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?
These are specializations of Function for primitive return types — they accept any reference type and return a primitive without boxing. ToIntFunction<String> applies String::length returning int without boxing overhead. Without these, you would use Function<String, Integer> 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?
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<T, T>?
UnaryOperator<T> extends Function<T, T> with apply(T) returning T. It is semantically clearer — when the input and output types are the same, UnaryOperator documents this intent. Function<T, T> 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<T, T> for general single-type transformations."
12. What is the difference between IntFunction<R> and Function<Integer, R>?
IntFunction<R> takes a primitive int and returns R — no boxing of the input. Function<Integer, R> takes the Integer wrapper type — boxing occurs on every call. IntFunction<R> is more efficient in streams: stream.mapToObj(IntFunction<String>) 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?
BinaryOperator.minBy(Comparator<T>) returns a BinaryOperator<T> 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<T> and similar specialized consumers?
ObjDoubleConsumer<T> 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<String> 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?
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?
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?
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<T, U, V>. This is the composition pattern for two-argument functions."
18. What is the difference between IntPredicate and Predicate<Integer> in terms of performance?
IntPredicate takes a primitive int and returns boolean — no boxing of the input. Predicate<Integer> 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?
A Supplier for lazy initialization produces a value on demand — Supplier<Connection> 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?
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 standard toolkit for functional programming in Java 8+. Before this package, projects typically rolled their own single-method interfaces for callbacks, transformations, and predicates. The JDK’s built-in set handles the common cases: Predicate for boolean tests, Function for transformations, Supplier for lazy production, Consumer for side effects, and their binary variants. These types show up throughout the JDK and most Java libraries. Spring uses Function<T, R> for bean transformers, Jackson uses Predicate for property filters, and reactive libraries use Function for map operations. Once you know these types, you’ll start seeing them everywhere.

Function composition with andThen and compose is worth understanding. andThen chains operations in execution order (apply this, then apply the other), while compose applies the other function first (right-to-left). For transformation pipelines, chaining functions with andThen produces readable, maintainable code.

The primitive specializations (IntPredicate, IntFunction, IntSupplier, etc.) avoid boxing primitive values into wrapper objects. In tight loops or large stream pipelines, boxing overhead compounds quickly. If you’re writing stream.map(Integer::sum) on an IntStream, switch to mapToInt and sum() to stay in primitives.

These interfaces work closely with java.util.stream.Stream — every stream operation consumes one. They also work with java.util.Optional, where map takes a Function and flatMap takes a function returning Optional.


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