☕ 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

Intermediate Operations
Intermediate operations are Stream API methods that transform one stream into another stream, enabling pipeline construction through method chaining. Every intermediate operation is lazy — it does not process any elements when called; it only builds a description of the computation to be performed. The actual processing occurs only when a terminal operation triggers stream traversal, at which point all intermediate operations execute in a single pass over the data, interleaved element by element rather than stage by stage. This laziness and fusion model is the defining architectural feature of the Streams API, distinguishing it from eager collection-transformation approaches. Intermediate operations fall into categories: filtering (filter, distinct), transformation (map and its primitive variants, flatMap), ordering (sorted), size-limiting (limit, skip), peeking (peek), and the Java 9 additions for prefix-based selection (takeWhile, dropWhile). This entry covers the laziness model and why it matters, every intermediate operation with its exact semantics and performance characteristics, stateless versus stateful intermediate operations, short-circuiting operations and how they interact with infinite streams, the map/flatMap distinction, and operation ordering and its performance implications.
Terminal Operations
Terminal operations are Stream API methods that trigger the actual traversal and processing of a stream pipeline, producing a non-stream result — a value, a collection, a side effect, or nothing (void). A stream can have exactly one terminal operation; once invoked, the stream is consumed and cannot be reused (calling any further operation on it throws IllegalStateException). Terminal operations fall into categories: reduction (reduce, collect), matching (anyMatch, allMatch, noneMatch), finding (findFirst, findAny), counting (count), iteration with side effects (forEach, forEachOrdered), and array/collection materialization (toArray, toList). Some terminal operations are short-circuiting (anyMatch, findFirst) and can terminate before examining the entire stream; others (forEach, count in most cases, collect) must examine every element. This entry covers every terminal operation in depth, the reduce() method and its three overloads with the combiner function's role in parallel execution, the Collectors framework as the primary mechanism for complex terminal reduction, short-circuiting semantics and their interaction with infinite streams, the single-use constraint and its rationale, and the choice between equivalent terminal operations for performance and clarity.
Optional
Optional<T>, introduced in Java 8, is a container object that may or may not hold a non-null value, designed to make the possible absence of a value explicit in a method's type signature rather than implicit through a nullable return type. Optional.of(value) creates a present Optional and throws NullPointerException if value is null; Optional.empty() creates an absent Optional; Optional.ofNullable(value) creates either a present or empty Optional depending on whether value is null. The design intent, stated explicitly in the Javadoc, is as a return type for methods where the absence of a result is a valid and expected outcome, communicating that absence through the type system instead of through null, with the goal of reducing NullPointerException at the API boundary. Optional is explicitly not intended for use as a field type, a method parameter type, or inside collections, and the JDK team has stated that misuse in these contexts works against its design intent. This entry covers the complete Optional API including creation, value extraction, conditional execution, transformation and chaining, the primitive specializations OptionalInt/OptionalLong/OptionalDouble, and the established conventions for where Optional should and should not be used.
Default Methods
Default methods, introduced in Java 8, are methods declared in an interface with a body, marked with the default keyword, providing a concrete implementation that implementing classes inherit automatically without being forced to override it. Before Java 8, interfaces could declare only abstract methods, meaning adding any new method to an interface would break every existing implementation — a problem that became critical when the Streams API needed to add methods like forEach(), stream(), and removeIf() to the existing Collection-family interfaces without breaking the entire ecosystem of third-party collection implementations. Default methods solved this by allowing interface evolution: new methods can be added to an interface with a default implementation, and existing implementing classes continue to compile and work without modification, inheriting the default behavior until they choose to override it. This entry covers the design motivation and interface evolution problem default methods solve, the complete syntax and inheritance rules, the diamond problem and Java's resolution rules for conflicting default methods from multiple interfaces, the explicit disambiguation syntax using InterfaceName.super.method(), the interaction between default methods and abstract classes, and the static method companion feature introduced alongside default methods.