W3docs

Java Consumer and Supplier

Side-effecting Consumer and value-producing Supplier functional interfaces in Java.

Java Consumer and Supplier

Consumer<T> and Supplier<T> are the two functional interfaces for the non-pure corners of the four-corner taxonomy:

  • Consumer<T> takes a value and returns nothing — its job is the side effect (print, log, write, push into a collection).
  • Supplier<T> takes nothing and returns a value — its job is to produce a T lazily, on demand (default values, factories, randomness).

Both pair with the Function/Predicate chapters that came before: those returned a value from a value, these step into and out of the surrounding world. This chapter covers both interfaces because their APIs are tiny and their use sites overlap.

Consumer<T>

@FunctionalInterface
public interface Consumer<T> {
  void accept(T t);                                         // the only abstract method

  default Consumer<T> andThen(Consumer<? super T> after);
}

A Consumer is "do something with this T." The SAM is accept. The single default method andThen chains consumers so they run in sequence on the same input:

Consumer<String> log    = System.out::println;
Consumer<String> store  = audit::record;
Consumer<String> both   = log.andThen(store);
both.accept("hello");    // prints "hello", then audit.record("hello")

andThen does not short-circuit if the first consumer throws — it lets the exception propagate, and the second consumer never runs. That's the same semantics as writing the two calls in a try-less block: the failure stops the sequence.

Where Consumer<T> shows up

list.forEach(System.out::println);                 // Iterable.forEach(Consumer)
stream.forEach(System.out::println);               // Stream.forEach
optional.ifPresent(name -> log.info(name));         // Optional.ifPresent
queue.peek(System.out::println);                    // not a Consumer call, but the shape is the same

Anywhere the JDK says "do something with each element," the parameter is a Consumer<T> or a BiConsumer<K, V> for two-argument cases (most notably Map.forEach((k, v) -> ...)).

BiConsumer<T, U>

The two-argument variant:

BiConsumer<String, Integer> show = (k, v) -> System.out.println(k + " => " + v);
Map<String, Integer> scores = Map.of("alice", 1, "bob", 2);
scores.forEach(show);

BiConsumer has the same andThen default. There is no BiSupplier — a two-argument Supplier would just be a BiFunction<T, U, R>.

Primitive specialisations — IntConsumer, LongConsumer, DoubleConsumer

IntConsumer    printInt = System.out::println;       // accepts int, no boxing
LongConsumer   tally    = n -> total += n;
DoubleConsumer record   = d -> samples.add(d);

Same andThen semantics. IntStream.forEach accepts an IntConsumer, which is why a primitive stream can call your lambda without boxing.

There's also ObjIntConsumer<T>, ObjLongConsumer<T>, ObjDoubleConsumer<T> for the case where one argument is an object and the other is a primitive — Stream.collect(Supplier, BiConsumer, BiConsumer) and its primitive cousins use them.

Supplier<T>

@FunctionalInterface
public interface Supplier<T> {
  T get();                                                   // the only abstract method
}

That's the whole interface — no default methods, no andThen, no composition. The reason is that a Supplier is the simplest possible shape: zero inputs, one output, and the only thing you can do with it is call get().

Supplier<List<String>> empty = ArrayList::new;
Supplier<UUID>         id    = UUID::randomUUID;
Supplier<String>       expensive = () -> loadFromDb();

Where Supplier<T> shows up

Supplier is the JDK's way of writing lazy — "give me this value, but only when I need it":

opt.orElseGet(() -> loadDefault());                                  // lazy default
Objects.requireNonNullElseGet(value, () -> sentinel);                // lazy default for null
Stream.generate(() -> Math.random()).limit(5);                        // infinite stream of supplied values
logger.debug("expensive: {}", () -> serialiseGraph(state));           // lazy log argument
CompletableFuture.supplyAsync(() -> compute());                        // run the supplier on another thread

Every place a Supplier<T> appears in the JDK, the contract is "this value might never be needed." Optional.orElseGet only calls get() when the optional is empty; Stream.generate only calls it when the next element is demanded. That laziness is the whole point — a plain T argument would have already been computed by the time the method was invoked.

Primitive specialisations — IntSupplier, LongSupplier, DoubleSupplier, BooleanSupplier

IntSupplier     count   = () -> counter.getAndIncrement();
DoubleSupplier  random  = Math::random;
BooleanSupplier ready   = sensor::isReady;

Supplier<Boolean> works, but the primitive BooleanSupplier is what the JDK uses for short-circuit gates (Stream.iterate, IntStream.iterate in their three-arg form take a BooleanSupplier or IntPredicate as the hasNext test).

Supplier versus a plain T argument

The rule of thumb:

  • Pass a value when the cost of computing it is negligible or when you definitely need it.
  • Pass a Supplier<T> when the cost matters and the callee might not need the value.
opt.orElse(loadDefaultFromDb());          // bad: loadDefaultFromDb() runs whether opt is present or not
opt.orElseGet(() -> loadDefaultFromDb()); // good: loadDefaultFromDb() runs only when opt is empty

That difference is the single most common reason orElseGet is preferred over orElse in production code.

A worked example: Consumer.andThen, Supplier laziness, primitive variants

The program below builds two consumers and chains them with andThen, demonstrates the orElse vs orElseGet evaluation difference with a counter, generates a small stream from a Supplier, and pairs IntConsumer with IntStream.forEach so no autoboxing occurs.

java— editable, runs on the server

What to take from the run:

  • log.andThen(store) ran both consumers on the same input, in declaration order. The audit trail showed both calls; the chain became a single Consumer<String> you could pass to forEach like any other.
  • The andThen chain that started with boom halted at the exception — never was never invoked. andThen is sequential, not exception-swallowing.
  • present.orElseGet(expensive) left the supplier untouched because the optional was present, while present.orElse(expensive.get()) evaluated the expensive call before it was even needed. The call counter is the proof — that's the gap Supplier exists to bridge.
  • Stream.generate(ids).limit(3) produced three UUIDs by calling get() exactly three times. The supplier is the lazy source of an unbounded stream — limit is what makes the pipeline finite.
  • IntConsumer add plugged straight into IntStream.forEach and avoided boxing every integer in the range. Use the primitive specialisation whenever you're inside a primitive stream.
  • BooleanSupplier underFive showed the shape the JDK uses for Stream.iterate's three-argument form and other "keep going until" gates — the supplier is checked once per iteration, lazily.

What's next

You've now seen all four corners: Function (in, out), Predicate (in, boolean), Consumer (in, no out), Supplier (no in, out). The next chapter, Java BinaryOperator and UnaryOperator, closes the part with the two specialisations where every parameter shares the same type — the shape that powers Stream.reduce, Map.merge, and List.replaceAll.

Practice

Practice

You're writing `String name = userOpt.orElseXxx(...)` and the default value is `loadDefaultName()`, which takes several seconds because it hits a database. You want that load to run *only* if `userOpt` is empty. Which call is correct?