Supplier\
Introduced in Java 8 as part of the java.util.function package, the Supplier<T> interface represents a fundamental concept in functional programming: the provider of results. Unlike many other functional interfaces that focus on transforming or consuming data, a Supplier is purely a producer. It takes no input and returns a single result of type T.
At its core, the interface is defined by a single abstract method:
@FunctionalInterface
public interface Supplier<T> {
T get();
}
Because it is marked with @FunctionalInterface, it is perfectly suited for use with Lambda expressions and method references, making it a highly versatile tool for modern Java development.
Core Characteristics
The defining trait of a Supplier is its "on-demand" nature. The logic encapsulated within the get() method is not executed when the Supplier is defined, but rather when the get() method is explicitly invoked. This distinction is the foundation for lazy evaluation, a technique used to defer expensive computations until they are absolutely necessary.
Implementation Styles
There are two primary ways to instantiate a Supplier:
1. Using Lambda Expressions
Lambda expressions allow you to define the production logic inline. This is ideal for simple logic or when you need to capture variables from the surrounding scope.
// A supplier that returns a constant string
Supplier<String> greetingSupplier = () -> "Hello, World!";
System.out.println(greetingSupplier.get());
// A supplier that produces a new list instance every time it is called
Supplier<List<Integer>> listSupplier = () -> new ArrayList<>();
List<Integer> firstList = listSupplier.get();
List<Integer> secondList = listSupplier.get(); // A completely different instance
2. Using Method References
One of the most elegant uses of Supplier is through constructor references. If a class has a no-argument constructor, you can use the ClassName::new syntax to create a supplier that acts as a factory.
Supplier<ArrayList<String>> listFactory = ArrayList::new;
Supplier<Date> dateSupplier = Date::new;
Supplier<Person> personSupplier = Person::new;
// Usage
Person p = personSupplier.get();
Comparative Analysis
To understand Supplier<T> better, it is helpful to compare it with its "siblings" in the functional interface family. A simple way to remember their roles is: Supplier produces, Consumer consumes, and Function transforms.
| Interface | Method Signature | Input | Output | Primary Role |
|---|---|---|---|---|
Supplier<T> |
T get() |
None | T |
Production |
Consumer<T> |
void accept(T t) |
T |
None | Consumption |
Function<T, R> |
R apply(T t) |
T |
R |
Transformation |
Predicate<T> |
boolean test(T t) |
T |
boolean |
Validation |
Practical Application Scenarios
The power of Supplier<T> lies in its ability to decouple the definition of a task from its execution.
1. Optimizing Performance via Lazy Evaluation
A classic example of Supplier in action is the Optional.orElseGet method. In Java, Optional.orElse(T other) takes a direct value. This means the value is computed immediately, even if the Optional is not empty.
In contrast, Optional.orElseGet(Supplier<? extends T> other) takes a Supplier. The logic inside the supplier is only executed if the Optional is actually empty.
String name = null;
// BAD: getDefault() is called even if name is NOT null (unnecessary overhead)
String a = Optional.ofNullable(name).orElse(getDefault());
// GOOD: getDefault() is only called if name is null (lazy evaluation)
String b = Optional.ofNullable(name).orElseGet(() -> getDefault());
2. Generating Infinite Streams
The Stream.generate(Supplier<T> s) method uses a Supplier to produce elements for a stream. Since the supplier can keep producing values indefinitely, it is almost always paired with a limit() operation to prevent infinite loops.
// Generating a stream of 5 random integers
List<Integer> randomNumbers = Stream.generate(() -> new Random().nextInt(100))
.limit(5)
.collect(Collectors.toList());
3. Implementing a Lightweight Factory Pattern
Instead of using complex class hierarchies for object creation, you can use a Map of Supplier objects to implement a registry. This adheres to the Open/Closed Principle, as you can add new types without modifying existing logic.
Map<String, Supplier<Product>> productRegistry = new HashMap<>();
productRegistry.put("phone", Phone::new);
productRegistry.put("laptop", Laptop::new);
// Creating a product based on a key
Product p = productRegistry.get("phone").get();
4. Asynchronous Programming
In the realm of concurrency, CompletableFuture.supplyAsync(Supplier<U> supplier) is used to run a task in a separate thread and return a result once the task completes.
CompletableFuture<String> futureResult = CompletableFuture.supplyAsync(() -> {
// Simulate a heavy database query
return queryFromDatabase();
});
Specialized Primitive Suppliers
To avoid the performance overhead of auto-boxing (converting primitives like int to their wrapper classes like Integer), Java provides specialized versions of the Supplier interface for primitive types:
BooleanSupplier: Providesboolean getAsBoolean()IntSupplier: Providesint getAsInt()LongSupplier: Provideslong getAsLong()DoubleSupplier: Providesdouble getAsDouble()
Using these specialized interfaces ensures that your high-performance applications remain efficient by working directly with primitive values.
Important Considerations and Best Practices
While Supplier<T> is powerful, there are a few nuances to keep in mind:
- Exception Handling: The
get()method does not declare any checked exceptions. If your production logic throws a checked exception, you must catch it within the lambda and wrap it in an unchecked exception (likeRuntimeException). - Object Identity: A
Supplierdoes not guarantee whether it returns a new instance or a shared instance. The behavior is entirely dependent on the implementation. For example,() -> new ArrayList<>()creates a new object every time, while() -> Constant.INSTANCEreturns the same object. - Logging Optimization: Many modern logging frameworks (like SLF4J 2.x) support
Supplier<String>. By passing a supplier to a log method, the expensive string concatenation only occurs if the specific log level (e.g.,DEBUG) is actually enabled, saving significant CPU cycles.
Summary
The Supplier<T> interface is a minimalist yet potent tool in the Java developer's toolkit. By providing a standard way to represent "producers," it enables sophisticated patterns like lazy evaluation, efficient factory registries, and streamlined asynchronous processing. Mastering its usage is a vital step toward writing more expressive, efficient, and modern Java code.