1 minute read

Java 8 introduced the Stream API, a major shift toward functional-style programming in Java. Among the key design elements that power this shift are the functional interfaces like Consumer, Supplier, and Function. While the Stream API itself gets much attention, the underlying design of these interfaces reveals an elegant model for data production and consumption.

The Role of Consumer<T>

The Consumer<T> interface represents an operation that takes a single input and returns no result. It is typically used for side-effect-based consumption:

List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
names.forEach(name -> System.out.println(name));

In this example, forEach accepts a Consumer — an action that gets called with each element of the stream. This allows clean, declarative data traversal and side-effect execution without requiring external loops.

The Uniqueness of Supplier<T>

Supplier<T> is the mirror image of Consumer. It represents a data source — a function that provides a value without taking any input:

Supplier<Double> randomSupplier = () -> Math.random();
System.out.println(randomSupplier.get());

This is a powerful abstraction, especially in cases where you want to defer computation or lazily generate values. For instance, in testing, configuration, or infinite streams:

Stream.generate(() -> Math.random())
      .limit(5)
      .forEach(System.out::println);

Here, generate uses a Supplier to feed the stream on-demand — emphasizing a key API design feature: push vs pull. Consumer pulls data from the stream to act upon, while Supplier pushes data into the stream for consumption.

Stream Design: Composability by Contract

The stream API makes heavy use of higher-order functions and pure interfaces. The division between Function, Consumer, and Supplier helps the runtime and developers enforce clear intent:

  • Use Consumer<T> when you want to perform an action on each element
  • Use Supplier<T> when you want to provide data, lazily or repeatedly

This leads to a predictable and consistent API experience:

Stream<Integer> stream = Stream.generate(() -> 42);
stream.limit(3).forEach(System.out::println);  // Uses Supplier and Consumer

Why This Matters

Unlike traditional imperative loops, the Stream API, powered by these functional interfaces, allows for lazy evaluation, parallelization, and cleaner code paths. It encourages users to think in terms of data flow, not control flow.

And perhaps most importantly, this design aligns with stateless computation, helping developers write thread-safe and predictable code in parallel stream contexts.

Final Thoughts

Java 8’s Stream API might have borrowed ideas from functional languages, but its true strength lies in how it harmonizes those ideas with Java’s object-oriented roots. The clear roles of Consumer and Supplier are a great example of how well-designed abstractions can enable expressive, safe, and performant code.


Published on Apr 17, 2018 — written by Ganesh Raman.

Comments