Lambdas & Functional Interfaces

Consumer & Supplier

15 min Lesson 6 of 13

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:

void accept(T t);

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.

import java.util.function.Consumer; public class ConsumerDemo { public static void main(String[] args) { Consumer<String> print = message -> System.out.println(">> " + message); print.accept("Hello, Consumer!"); // >> Hello, Consumer! print.accept("Side effects only"); // >> Side effects only } }

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.

Consumer<String> log = s -> System.out.println("[LOG] " + s); Consumer<String> notify = s -> System.out.println("[NOTIFY] " + s); Consumer<String> logAndNotify = log.andThen(notify); logAndNotify.accept("Order shipped"); // [LOG] Order shipped // [NOTIFY] Order shipped
Key insight: unlike 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>:

import java.util.function.BiConsumer; BiConsumer<String, Integer> printWithScore = (name, score) -> System.out.printf("%s scored %d%n", name, score); printWithScore.accept("Alice", 95); // Alice scored 95 printWithScore.accept("Bob", 82); // Bob scored 82

Map::forEach is the most common place you encounter BiConsumer in the standard library — it passes each key and value to your lambda.

import java.util.Map; Map<String, Integer> scores = Map.of("Alice", 95, "Bob", 82, "Carol", 88); scores.forEach((name, score) -> System.out.printf("%-8s %d%n", name, score) );

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:

T get();

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.

import java.util.function.Supplier; import java.time.LocalDateTime; public class SupplierDemo { public static void main(String[] args) { // Supplier wrapping a potentially expensive computation Supplier<LocalDateTime> now = () -> LocalDateTime.now(); System.out.println("Before call"); // LocalDateTime.now() has NOT been called yet LocalDateTime timestamp = now.get(); // called exactly here System.out.println("Captured: " + timestamp); } }
When to choose a Supplier over just a value: pass a 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:

import java.util.Optional; Optional<String> maybe = Optional.empty(); // orElse: the fallback value is computed EAGERLY — even if Optional is present String a = maybe.orElse(expensiveDefault()); // orElseGet: the Supplier is called ONLY when Optional is empty — lazy String b = maybe.orElseGet(() -> expensiveDefault());

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.

Common pitfall: a 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:

import java.util.List; import java.util.function.Consumer; import java.util.function.Supplier; public class BehaviourInjection { // Accept a Consumer to decouple the action from this method static void processAll(List<String> items, Consumer<String> action) { for (String item : items) { action.accept(item); } } // Accept a Supplier to defer object creation static void printIfAbsent(String value, Supplier<String> fallback) { System.out.println(value != null ? value : fallback.get()); } public static void main(String[] args) { processAll( List.of("apple", "banana", "cherry"), item -> System.out.println(item.toUpperCase()) ); printIfAbsent(null, () -> "default-" + System.currentTimeMillis()); } }

Quick reference

  • Consumer<T> — takes T, returns nothing. Use for side effects.
  • Consumer::andThen — chains two consumers on the same input.
  • BiConsumer<T, U> — takes T and U, returns nothing.
  • Supplier<T> — takes nothing, produces T. Use for lazy / deferred values.
  • Prefer orElseGet(Supplier) over orElse(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.