☕ Java

Consumer

Consumer<T> is a functional interface in java.util.function that represents an operation that accepts a single input argument and returns no result. It is the standard abstraction for side-effect-oriented operations — printing, logging, persisting, modifying in-place, or any action that consumes a value without producing a new one. Consumer<T> declares one abstract method, accept(T t), and one default method, andThen(Consumer<? super T> after), which composes two consumers sequentially: the second consumer receives the same input as the first, enabling chaining of multiple side effects on the same value. The java.util.function package also provides primitive specializations — IntConsumer, LongConsumer, DoubleConsumer — that avoid boxing overhead, and BiConsumer<T, U> for operations on two arguments. Consumer is used throughout the Stream API (forEach), the Collections API (Iterable.forEach, Map.forEach), and is the standard type for callback and event-handler parameters. This entry covers the full Consumer API including andThen composition, all primitive specializations and BiConsumer, the distinction between Consumer and other functional interfaces, common usage patterns in streams and collections, and how to write effective Consumer implementations.

Consumer<T> — accept() and andThen() Composition

Consumer<T> is a @FunctionalInterface with one abstract method: void accept(T t). The void return type is what distinguishes Consumer from Function<T, R> — a Consumer produces no result, only a side effect. A lambda assigned to Consumer<String> must take a String parameter and return nothing. A method reference assigned to Consumer<String> must refer to a method that takes a String and returns void, or to an instance method of String that takes no parameters and is called on the input. The accept(T t) method is the sole abstract method. Consumer implementations are invoked by calling accept() directly or, more commonly, by passing them to APIs that accept Consumer parameters (forEach, peek, ifPresent, etc.). The contract: accept should perform its side effect on t. No contract governs whether accept modifies t, interacts with external state, or throws unchecked exceptions. The andThen(Consumer<? super T> after) default method composes two consumers into one. The returned Consumer applies this consumer first, then applies after to the same input. Composition is left-to-right: the first consumer's effect happens before the second's. This allows building pipelines of side effects: validate → log → persist, or print → write → notify. The composed Consumer is a single object that can be passed to any API accepting Consumer<T>. If this consumer throws an exception, after is not called. The use of ? super T in the andThen parameter allows chaining consumers of more general types. A Consumer<String> can be composed with a Consumer<Object> via andThen: the Consumer<Object> accepts String (because String is an Object) and handles it as an Object. This follows the PECS principle: Consumer is a consumer of T, so its parameter uses ? super T.
Java
// ── Basic Consumer lambda ─────────────────────────────────────────────
import java.util.function.*;

Consumer<String> printer  = s -> System.out.println(s);
Consumer<String> upperPrinter = s -> System.out.println(s.toUpperCase());

printer.accept("hello");       // prints: hello
upperPrinter.accept("hello");  // prints: HELLO

// ── Method reference as Consumer ─────────────────────────────────────
Consumer<String> mrefPrinter = System.out::println;  // instance method ref on PrintStream
Consumer<String> mrefUpper   = String::toUpperCase;  // WRONG — toUpperCase() returns String, not void

// Correct method reference: static method with void return + one parameter:
Consumer<String> logger = MyLogger::log;  // static void log(String msg)

// Instance method of the input object:
Consumer<List<String>> listClearer = List::clear;   // void clear() called on the list
List<String> words = new ArrayList<>(List.of("a","b","c"));
listClearer.accept(words);
System.out.println(words);  // []

// ── andThen: sequential composition ──────────────────────────────────
Consumer<String> validate = s -> {
    if (s == null || s.isEmpty()) throw new IllegalArgumentException("Empty string");
};
Consumer<String> log      = s -> System.out.println("Processing: " + s);
Consumer<String> persist  = s -> database.save(s);

// Compose into a single Consumer:
Consumer<String> pipeline = validate.andThen(log).andThen(persist);
pipeline.accept("Alice");   // validate, then log, then persist — left to right

// Exception in first consumer stops the chain:
try {
    pipeline.accept("");    // validate throws → log and persist NOT called
} catch (IllegalArgumentException e) {
    System.out.println("Validation failed: " + e.getMessage());
}

// ── andThen with ? super T: compose with more general consumer ────────
Consumer<String>  stringConsumer  = s -> System.out.println("String: " + s);
Consumer<Object>  objectConsumer  = o -> System.out.println("Object: " + o.getClass().getSimpleName());

// Consumer<String>.andThen(Consumer<? super String>) = Consumer<String>.andThen(Consumer<Object>)
Consumer<String> composed = stringConsumer.andThen(objectConsumer);
composed.accept("test");
// String: test
// Object: String

// ── Building a processing pipeline with andThen ───────────────────────
Consumer<User> auditLog     = user -> audit.log("Access: " + user.id());
Consumer<User> updateCache  = user -> cache.put(user.id(), user);
Consumer<User> sendWelcome  = user -> emailService.sendWelcome(user.email());

Consumer<User> onNewUser = auditLog.andThen(updateCache).andThen(sendWelcome);
// Pass the composed consumer anywhere a Consumer<User> is accepted:
newUsers.forEach(onNewUser);

Primitive Specializations and BiConsumer

The primitive specializations IntConsumer, LongConsumer, and DoubleConsumer avoid the boxing overhead of Consumer<Integer>, Consumer<Long>, and Consumer<Double>. Their single abstract method is void accept(int value), void accept(long value), and void accept(double value) respectively. Each also has andThen() composition. ObjIntConsumer<T>, ObjLongConsumer<T>, and ObjDoubleConsumer<T> are bivariate specializations where the first argument is a reference type and the second is a primitive — their abstract method is void accept(T t, int value) etc. BiConsumer<T, U> represents an operation on two arguments with no return. Its abstract method is void accept(T t, U u). It also has andThen(BiConsumer<? super T, ? super U> after) for composition. BiConsumer is commonly used with Map.forEach(BiConsumer<K, V>), which passes each key-value pair to the BiConsumer. It is also used for event handlers that carry a context object and an event object, builder-style consumers that update a target object using a value, and any operation naturally described by two inputs. The distinction between Consumer and other functional interfaces: Consumer<T> is for side effects with no result; Function<T, R> produces a result without required side effects; Predicate<T> tests a condition and returns boolean; Supplier<T> produces a value with no input; UnaryOperator<T> transforms T to T; BiFunction<T, U, R> is Consumer with a result. When writing an API, choose the most specific interface: if the operation must produce a result, use Function; if it must only perform an action, use Consumer. This makes intent clear to callers and prevents misuse (a caller cannot accidentally ignore the Consumer's return value — there is none).
Java
// ── IntConsumer: primitive int, no boxing ─────────────────────────────
IntConsumer printInt    = i -> System.out.println("int: " + i);
IntConsumer doubleAndPrint = i -> System.out.println("doubled: " + (i * 2));

printInt.accept(42);        // int: 42
doubleAndPrint.accept(42);  // doubled: 84

IntConsumer both = printInt.andThen(doubleAndPrint);
both.accept(10);
// int: 10
// doubled: 20

// ── Primitive specialization performance ─────────────────────────────
// Consumer<Integer>: each call boxes the int to Integer → heap allocation
Consumer<Integer> boxedConsumer = i -> System.out.println(i);
boxedConsumer.accept(42);   // 42 (int) → Integer.valueOf(42) → auto-unboxed for println

// IntConsumer: no boxing — int passed directly
IntConsumer primitiveConsumer = i -> System.out.println(i);
primitiveConsumer.accept(42);  // no allocation

// In a loop over 1,000,000 ints:
// Consumer<Integer>: 1M Integer allocations → GC pressure
// IntConsumer:       0 allocations → significantly faster

// ── LongConsumer and DoubleConsumer ──────────────────────────────────
LongConsumer   printLong   = l -> System.out.printf("long:   %d%n", l);
DoubleConsumer printDouble = d -> System.out.printf("double: %.4f%n", d);

printLong.accept(System.currentTimeMillis());
printDouble.accept(Math.PI);

// ── BiConsumer<T, U>: two-argument consumer ───────────────────────────
BiConsumer<String, Integer> printKeyValue = (key, value) ->
    System.out.printf("%-20s: %d%n", key, value);

printKeyValue.accept("Count",      42);
printKeyValue.accept("Total",   1000);
printKeyValue.accept("Average",   25);

// ── BiConsumer with Map.forEach ───────────────────────────────────────
Map<String, Integer> scores = Map.of(
    "Alice", 95, "Bob", 87, "Carol", 92);

// Map.forEach signature: void forEach(BiConsumer<? super K, ? super V> action)
scores.forEach((name, score) ->
    System.out.printf("%-10s scored %d%n", name, score));

// BiConsumer composition with andThen:
BiConsumer<String, Integer> log    = (k, v) -> System.out.println("Logging: " + k + "=" + v);
BiConsumer<String, Integer> cache  = (k, v) -> localCache.put(k, v);
BiConsumer<String, Integer> both   = log.andThen(cache);

scores.forEach(both);   // log AND cache each entry

// ── ObjIntConsumer: reference + primitive bivariate ───────────────────
ObjIntConsumer<List<String>> insertAt = (list, index) ->
    list.add(index, "INSERTED");

List<String> data = new ArrayList<>(List.of("a", "b", "c"));
insertAt.accept(data, 1);
System.out.println(data);   // [a, INSERTED, b, c]

Consumer in Stream API, Collections, and Design Patterns

Consumer is the parameter type for the most commonly used Stream terminal operation: Stream.forEach(Consumer<? super T> action) passes each element to the Consumer's accept method. Stream.peek(Consumer<? super T> action) is an intermediate operation that also uses Consumer, allowing side effects (logging, debugging) without changing stream elements. Iterable.forEach(Consumer<? super T> action) is available on all Collections, providing an alternative to enhanced-for loops for side-effect-only iteration. The Observer pattern and event listener pattern map naturally to Consumer. An event source registers Consumer instances as listeners; when an event occurs, the source calls consumer.accept(event). Compared to defining a dedicated @FunctionalInterface for each event type, using Consumer<EventType> allows callers to pass lambda expressions directly without any additional interface definitions. Consumer is useful for the Builder pattern's "configure using a Consumer" idiom: a factory method accepts Consumer<Builder> to let the caller configure the builder without exposing the builder's type in every API boundary. This is seen in AssertJ's assertThat, Spring's WebTestClient, and various testing frameworks: assertThat(user, user -> { assertThat(user.name()).isEqualTo("Alice"); assertThat(user.age()).isEqualTo(30); }). The distinction from Runnable: Runnable takes no arguments and returns nothing; Consumer takes one argument and returns nothing. Use Consumer when the side effect depends on the argument; use Runnable when it does not. Both are @FunctionalInterface and both accept lambda expressions that perform side effects.
Java
// ── Consumer in Stream.forEach ────────────────────────────────────────
List<String> names = List.of("Alice", "Bob", "Carol", "Dave");

// lambda Consumer in forEach:
names.stream().forEach(name -> System.out.println("Hello, " + name));

// method reference Consumer in forEach:
names.stream().forEach(System.out::println);

// ── Stream.peek: Consumer for debugging (intermediate) ────────────────
List<Integer> result = List.of(1, 2, 3, 4, 5).stream()
    .peek(n -> System.out.println("Before filter: " + n))
    .filter(n -> n % 2 == 0)
    .peek(n -> System.out.println("After filter:  " + n))
    .collect(Collectors.toList());
// Before filter: 1
// Before filter: 2    After filter: 2
// Before filter: 3
// Before filter: 4    After filter: 4
// Before filter: 5

// ── Iterable.forEach on Collections ──────────────────────────────────
Map<String, List<String>> groupedData = new HashMap<>();
groupedData.put("fruits",  List.of("apple", "banana"));
groupedData.put("veggies", List.of("carrot", "pea"));

// Map.forEach: BiConsumer<K, V>
groupedData.forEach((category, items) -> {
    System.out.println(category + ": " + items);
});

// ── Observer pattern with Consumer ───────────────────────────────────
public class EventBus<T> {
    private final List<Consumer<T>> listeners = new ArrayList<>();

    public void subscribe(Consumer<T> listener) {
        listeners.add(listener);
    }

    public void publish(T event) {
        listeners.forEach(listener -> listener.accept(event));
    }
}

EventBus<String> bus = new EventBus<>();
bus.subscribe(msg -> System.out.println("Logger: " + msg));
bus.subscribe(msg -> auditService.record(msg));
bus.subscribe(msg -> notificationService.send(msg));

bus.publish("User logged in");
// Logger: User logged in
// (audit recorded)
// (notification sent)

// ── Builder configure-with-Consumer idiom ────────────────────────────
public class HttpRequest {
    private String method, url, body;
    private Map<String, String> headers = new HashMap<>();

    private HttpRequest() {}

    public static HttpRequest create(Consumer<HttpRequest> configurator) {
        HttpRequest req = new HttpRequest();
        configurator.accept(req);  // caller configures the request
        req.validate();
        return req;
    }

    public HttpRequest method(String m)      { this.method  = m; return this; }
    public HttpRequest url(String u)          { this.url     = u; return this; }
    public HttpRequest body(String b)         { this.body    = b; return this; }
    public HttpRequest header(String k, String v) { headers.put(k, v); return this; }
    private void validate() { Objects.requireNonNull(method); Objects.requireNonNull(url); }
}

HttpRequest req = HttpRequest.create(r -> r
    .method("POST")
    .url("/api/users")
    .header("Content-Type", "application/json")
    .body("{"name":"Alice"}")
);

Related Topics in Java 8 Features

Lambda Expressions
Lambda expressions, introduced in Java 8, are anonymous functions — blocks of code that can be stored in variables, passed as arguments, and returned from methods, treating behavior as data. A lambda has three parts: a parameter list, an arrow token (->), and a body. The body is either a single expression (whose value is the implicit return value) or a block of statements wrapped in braces. Lambdas implement functional interfaces — interfaces with exactly one abstract method — allowing any lambda whose signature matches the abstract method's signature to be used wherever that interface is expected. The lambda syntax is syntactic sugar: every lambda is compiled to an invocation of the functional interface's abstract method, with the compiler generating a class (via invokedynamic) that implements the interface and delegates to the lambda body. This entry covers the complete lambda syntax including all shorthand forms, variable capture and the effectively-final constraint, method references as a specialized lambda syntax, the relationship between lambdas and the type system, how lambdas interact with exception handling, the invokedynamic compilation strategy and its performance characteristics, and the complete set of rules governing lambda type inference.
Functional Interfaces
A functional interface is any Java interface that has exactly one abstract method. This single-abstract-method (SAM) contract makes the interface a valid target type for a lambda expression or method reference — the lambda provides the implementation of that one abstract method. The @FunctionalInterface annotation is optional but strongly recommended: it causes the compiler to verify that the interface satisfies the SAM constraint, rejecting it at compile time if there is more than one abstract method. The java.util.function package, introduced in Java 8, provides 43 standard functional interfaces organized around four root types — Function, Consumer, Supplier, Predicate — and their variations for primitives (IntFunction, LongSupplier, DoubleConsumer, etc.), binary operations (BiFunction, BiConsumer, BiPredicate), and unary operators (UnaryOperator, IntUnaryOperator, etc.). This entry covers the design principles behind functional interfaces, the complete @FunctionalInterface contract including default and static methods, the full java.util.function hierarchy and the pattern that governs naming, creating custom functional interfaces with checked exceptions, composing functional interfaces via default methods, and the relationship between functional interfaces and the type system including the rules for lambda assignment and widening.
Predicate
Predicate<T> is a functional interface in java.util.function representing a boolean-valued function of one argument, with the single abstract method boolean test(T t). It is one of the four foundational functional interfaces in the Java standard library and is used throughout the Collections framework, Streams API, and Optional for filtering, condition testing, and validation. Predicate is designed for composition: its default methods and(Predicate), or(Predicate), and negate() allow building complex boolean expressions from simple predicates without boilerplate. The static methods isEqual(Object) and not(Predicate) provide factory methods for common cases. The primitive specializations IntPredicate, LongPredicate, and DoublePredicate avoid boxing overhead for numeric values. BiPredicate<T,U> extends the concept to two-argument boolean functions. This entry covers the complete Predicate API, all composition methods and their short-circuit semantics, the static factory methods, primitive specializations, BiPredicate, using Predicate in stream pipelines and Collections methods, building validation frameworks with Predicate composition, and the performance and readability trade-offs of different composition styles.
Function
Function<T,R> is a functional interface in java.util.function representing a function that accepts one argument of type T and produces a result of type R, with the single abstract method R apply(T t). It is the most general transformation interface in the standard library, used throughout the Streams API for mapping (Stream.map()), in Optional for value transformation (Optional.map(), Optional.flatMap()), and as a building block for more specialized functional interfaces. Function provides two default composition methods — andThen() and compose() — that create new functions by chaining two functions together, enabling functional pipeline construction without intermediate variables. The specializations cover all combinations of generic and primitive inputs and outputs: ToIntFunction, IntFunction, IntToLongFunction, and so on. UnaryOperator<T> extends Function<T,T> for operations that transform a value within the same type. BiFunction<T,U,R> generalizes to two input arguments. This entry covers the complete Function API, the semantics of andThen versus compose, all specializations and when each is appropriate, the functional relationship between Function and other java.util.function types, partial application patterns, and Function as the basis for building data pipelines.