A Curious Question
Some time ago, a junior developer I was working with asked an interesting question:
"I know that passing a Supplier<T>
to a method allows for lazy loading, but what happens under the hood? If the supplier calls a service method that fetches data from a REST API, is the API call made immediately? If not, how does dependency injection work in this case?"
This was a great question because it touched on multiple important concepts: functional interfaces, lazy evaluation, and dependency injection. While many Java developers use Supplier<T>
in their code, not everyone fully understands its inner workings. So, let’s dive into it.
A Quick Refresher: What is Supplier<T>
?
Supplier<T>
is a functional interface in Java that represents a function that takes no arguments but returns a value of type T
.
Here’s its simple definition from java.util.function
:
@FunctionalInterface
public interface Supplier<T> {
T get();
}
It has a single abstract method,
get()
, which returns a value but does not take any parameters.It is commonly used for lazy evaluation, deferred execution, and factory methods.
Basic Example
To see Supplier<T>
in action, let’s define one that generates a greeting:
Supplier<String> greetingSupplier = () -> "Hello, World!";
System.out.println(greetingSupplier.get()); // Only now is the value generated
Since Supplier<T>
does not execute immediately, it allows us to delay computations until they are actually needed.
What Happens at the JVM Level?
When you pass a Supplier<T>
to a method, you are passing a lambda expression (or a method reference) that represents a piece of code to be executed later.
At the JVM level:
The lambda expression or method reference is converted into an instance of a functional interface (
Supplier<T>
).If the lambda captures variables from the enclosing scope, the compiler generates a synthetic class that holds references to those captured variables.
The actual code inside the lambda or method reference is not executed immediately. Instead, it is stored as an instance of
Supplier<T>
and invoked later using.get()
.
Example:
Supplier<String> lazySupplier = () -> "Hello, World!";
At runtime, the JVM creates an instance of a synthetic class implementing Supplier<String>
, where get()
returns "Hello, World!"
when called.
Understanding the Lazy Execution of Supplier<T>
Let’s consider a slightly more interesting example. Suppose we have a method that retrieves data from an external service:
class DataService {
public String fetchData() {
System.out.println("Fetching data...");
return "Data from API";
}
}
If we create a Supplier<String>
using a method reference:
DataService service = new DataService();
Supplier<String> dataSupplier = service::fetchData;
What happens here?
No API call is made yet –
service::fetchData
is just a reference, not an execution.When
.get()
is called, only then doesfetchData()
execute.
Here’s a full example demonstrating this behavior:
public class LazyLoadingExample {
public static void main(String[] args) {
DataService service = new DataService();
Supplier<String> dataSupplier = service::fetchData;
System.out.println("Before calling get()");
String result = dataSupplier.get(); // Triggers the actual API call
System.out.println("Result: " + result);
}
}
Console Output:
Before calling get()
Fetching data...
Result: Data from API
As you can see, fetchData()
is not executed at the time of supplier creation, only when .get()
is called.
How Dependency Injection Works with Supplier<T>
A natural follow-up question is:
"If the service is injected using a dependency injection (DI) framework like Spring, will it still work?"
Let’s see how Supplier<T>
interacts with DI.
Spring Example
@Service
class DataService {
public String fetchData() {
System.out.println("Fetching data from REST API...");
return "Data from API";
}
}
@Component
class ConsumerComponent {
private final DataService dataService;
@Autowired
public ConsumerComponent(DataService dataService) {
this.dataService = dataService;
}
public void execute() {
Supplier<String> dataSupplier = dataService::fetchData;
System.out.println("Before calling get()");
System.out.println("Fetched: " + dataSupplier.get()); // Triggers API call
}
}
Spring’s dependency injection ensures that dataService
is available when needed, and lazy evaluation means that the API call happens only when requested.
Real-World Use Cases for Supplier<T>
Avoiding Unnecessary Computations
public void process(Supplier<String> expensiveOperation) {
if (shouldUseResult()) {
System.out.println(expensiveOperation.get()); // Only called if needed
}
}
Lazy Initialization
private Supplier<Connection> connectionSupplier = this::createConnection;
Deferred Logging
private static void log(Supplier<String> message) {
if (isDebugEnabled()) {
System.out.println(message.get());
}
}
Key Takeaways
Supplier<T>
is a functional interface that provides a value on demand.When passing a method reference (
service::fetchData
), no execution happens until.get()
is called.In Spring DI, injected services work as expected, and lazy evaluation ensures API calls are made only when needed.
Common use cases include avoiding unnecessary computations, lazy initialization, and performance optimization.
Understanding how Supplier<T>
works under the hood helps us write more efficient and performant code while leveraging modern Java practices.
If you’ve used Supplier<T>
in an interesting way, I’d love to hear about it! Let’s continue the discussion in the comments.
Articles I enjoyed this week
Apache Kafka 🔥 Your Gateway to High-Demand Jobs - Sketech #22 by Nina
Databricks Photon Engine: Boosting Query Performance and Optimizing Big Data Processing by Lorenzo Bradanini
Great post, Riccardo! and thanks a lot for the mention