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 aTlazily, 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 sameAnywhere 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 threadEvery 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 emptyThat 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.
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 singleConsumer<String>you could pass toforEachlike any other.- The
andThenchain that started withboomhalted at the exception —neverwas never invoked.andThenis sequential, not exception-swallowing. present.orElseGet(expensive)left the supplier untouched because the optional was present, whilepresent.orElse(expensive.get())evaluated the expensive call before it was even needed. The call counter is the proof — that's the gapSupplierexists to bridge.Stream.generate(ids).limit(3)produced three UUIDs by callingget()exactly three times. The supplier is the lazy source of an unbounded stream —limitis what makes the pipeline finite.IntConsumer addplugged straight intoIntStream.forEachand avoided boxing every integer in the range. Use the primitive specialisation whenever you're inside a primitive stream.BooleanSupplier underFiveshowed the shape the JDK uses forStream.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
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?