Consumer & Supplier
Consumer & Supplier
In the previous lessons you met Predicate (test something, return a boolean) and Function (transform one value into another). Two more built-in functional interfaces round out the everyday toolkit: Consumer and Supplier. They sit at opposite ends of the data-flow spectrum — one only takes data in, and the other only produces data out.
Consumer — acting on a value without returning one
java.util.function.Consumer<T> accepts one argument of type T and returns nothing (void). Its single abstract method is:
Use a Consumer whenever you want to perform a side effect — printing, logging, writing to a database, updating a UI element — without producing a new value back to the caller.
The lambda message -> System.out.println(...) compiles straight to Consumer<String> because it takes one argument and returns nothing. The method reference System.out::println works identically here and is often more readable.
Chaining Consumers with andThen
Consumer provides a default method andThen(Consumer<? super T> after) that chains two consumers so both act on the same input in sequence — useful when you want to log and persist, for example.
Function::andThen, which threads a transformed value from one function to the next, Consumer::andThen feeds the same original value to both consumers. No transformation happens — both consumers operate on identical input.
BiConsumer — two inputs, still no return
When you need to act on two values together, reach for BiConsumer<T, U>:
Map::forEach is the most common place you encounter BiConsumer in the standard library — it passes each key and value to your lambda.
Supplier — producing a value lazily
java.util.function.Supplier<T> takes no arguments and returns a value of type T. Its single abstract method is:
The signature communicates intent clearly: this object is a factory or a deferred computation. You hand the Supplier around and call get() only when you actually need the value — that is what lazy evaluation means.
Supplier when the value is expensive to compute and may not be needed (e.g. a fallback log message), or when you want a fresh value each time get() is called (e.g. a unique ID or a new object instance).
Practical example — orElseGet vs orElse in Optional
Optional's two fallback methods show why Supplier matters for performance:
If expensiveDefault() queries a database or calls a remote service, the difference between eager and lazy can be substantial. Prefer orElseGet with a Supplier whenever the fallback is non-trivial.
Supplier has no memory. Every call to get() re-executes the lambda body. If you want the result cached, compute it once and store it, or use a dedicated memoisation wrapper — do not assume Supplier caches automatically.
Passing behaviour to a method
Both interfaces shine when you inject behaviour into a method through parameters — the hallmark of functional programming style:
Quick reference
Consumer<T>— takesT, returns nothing. Use for side effects.Consumer::andThen— chains two consumers on the same input.BiConsumer<T, U>— takesTandU, returns nothing.Supplier<T>— takes nothing, producesT. Use for lazy / deferred values.- Prefer
orElseGet(Supplier)overorElse(value)for non-trivial fallbacks.
In the next lesson you will explore method references — a concise shorthand that converts existing methods directly into any of these functional interfaces.