DEV Community

Cover image for Surfing with FP Java - Mastering Supplier<T>
André Borba
André Borba

Posted on

Surfing with FP Java - Mastering Supplier<T>

Introduction

In the last episode, we mastered Consumer, the functional interface for performing actions.

Now, let’s turn our focus to Supplier, the simplest yet most powerful interface for lazy value generation.

If Predicate decides, Function transforms, and Consumer acts, then Supplier is the source. It provides values on demand, without requiring input.

What Is Supplier?

Here’s the definition:

@FunctionalInterface
public interface Supplier<T> {
    T get();
}
Enter fullscreen mode Exit fullscreen mode
  • Input: none.
  • Output: an object of type T.
  • Purpose: generate or supply values, often lazily or repeatedly.

Why Use Supplier?

Traditionally, values are produced eagerly:

String token = UUID.randomUUID().toString();
Enter fullscreen mode Exit fullscreen mode

With Supplier, we can defer execution and encapsulate value creation:

Supplier<String> tokenSupplier = () -> UUID.randomUUID().toString();

System.out.println(tokenSupplier.get()); // A new token each call
Enter fullscreen mode Exit fullscreen mode

This is especially powerful in lazy evaluation, configuration loading, testing, and dependency injection.

Practical Examples

1. Random or Unique Values

Suppliers naturally express randomness or unique generation:

Supplier<Integer> randomInt = () -> new Random().nextInt(100);

System.out.println(randomInt.get()); // e.g., 42
System.out.println(randomInt.get()); // e.g., 17
Enter fullscreen mode Exit fullscreen mode

2. Deferred Initialization

Delay expensive computation until it’s needed:

Supplier<List<User>> heavyQuery = () -> database.fetchAllUsers();

// Nothing happens yet
List<User> users = heavyQuery.get(); // Query executes only here
Enter fullscreen mode Exit fullscreen mode

3. Factories and Object Creation

Suppliers are elegant factories:

Supplier<User> newUser = () -> new User(UUID.randomUUID().toString());

User user1 = newUser.get();
User user2 = newUser.get();
Enter fullscreen mode Exit fullscreen mode

4. Stream Integration

Suppliers work with Stream.generate to produce infinite sequences:

Supplier<String> uuidSupplier = () -> UUID.randomUUID().toString();

List<String> ids = Stream.generate(uuidSupplier)
    .limit(3)
    .toList();

System.out.println(ids); 
// [550e8400-e29b-41d4-a716-446655440000, ...]
Enter fullscreen mode Exit fullscreen mode

Real-World Patterns

  1. Configuration Providers:
    Load config lazily when requested.

  2. Caching with Lazy Evaluation:
    Compute once, then reuse.

  3. Dependency Injection:
    Pass Suppliers instead of concrete objects for flexible object creation.

  4. Testing and Mocking:
    Suppliers make it easy to inject fake or mock data.

Best Practices

  • Keep Suppliers Pure: Avoid hidden side effects. Suppliers should primarily generate values.
  • Name Clearly: Use names like tokenSupplier, userFactory, configProvider.
  • Combine with Higher-Order Functions: Suppliers can be passed to methods to delay execution or customize behavior.

Common Pitfalls

  • Uncontrolled Infinite Streams: Stream.generate without a limit leads to infinite loops.
  • Heavy Logic Inside: Don’t hide complex operations inside Supplier#get(), it should be lightweight and predictable.
  • Side Effects: While possible, side-effect-driven suppliers (e.g., network calls) blur responsibilities. Prefer purity.

Functional Analogy

Think of Supplier as a well:

  • You don’t provide anything to it.
  • Each time you draw (get()), it gives you fresh water (T).
  • The well might be deep, infinite, or cached, but it always supplies.

Conclusion

Supplier is the lazy generator of functional Java.
It encapsulates value creation, supports lazy evaluation, and integrates seamlessly with streams, factories, and testing strategies.

By mastering Supplier, you unlock on-demand computation and flexible object creation.

What’s Next

In the next episode, we’ll dive into UnaryOperator, a specialized function for single-input transformations.
Where Function transforms from one type to another, UnaryOperator specializes in transforming within the same type, a cornerstone of functional composition.

Top comments (0)