- Understand that Supplier is a functional interface that provides values without taking any input
- Learn to use Supplier with lambda expressions for lazy value generation
- Apply the
get()method to retrieve supplied values - Use Suppliers for factory patterns and deferred execution
- Recognize when Suppliers are useful for expensive operations or random value generation
While Predicate tests conditions and Consumer performs actions, Supplier does something fundamentally different—it creates or provides values. Think of it as a source, a generator, a factory that produces something when asked. The interface takes no input, yet returns an output. This seemingly simple reversal of the typical function signature unlocks powerful patterns in Java development.
Supplier shines in scenarios where you want to defer the creation or computation of a value until it's actually needed.
Maybe you're generating random data, constructing complex objects, reading from external resources, or performing
expensive calculations. Rather than doing this work upfront and potentially wasting resources, you wrap the operation in
a Supplier and trigger it only when necessary. This lazy evaluation pattern appears throughout modern Java, from
Optional's orElseGet() to Stream's generation methods.
The beauty of Supplier lies in its simplicity and flexibility. One method, get(), produces a value on demand. No
parameters to worry about, no complex state to manage—just a clean abstraction for "give me something." Whether you're
building object factories, implementing lazy initialization, or generating test data, Supplier provides the functional
interface that makes these patterns type-safe and composable.
Supplier is a functional interface with a single abstract method that takes no arguments and returns a value:
@FunctionalInterface
public interface Supplier<T> {
T get();
}The generic type parameter T represents the type of value the supplier produces. A Supplier<String> generates
strings, a Supplier<Integer> produces integers, and a Supplier<Employee> creates employee objects. The method
signature—no parameters, one return value—makes Supplier the functional opposite of Consumer.
Because Supplier has exactly one abstract method, you can implement it with lambda expressions or method references. The functional interface annotation ensures this contract remains intact, catching any accidental additions of abstract methods at compile time.
Lambda expressions provide clean syntax for creating suppliers. The pattern follows () -> expression, where the
expression computes or produces the value to return:
package blog.academy.javapro;
import java.util.function.Supplier;
import java.util.Random;
public class SupplierBasics {
public static void main(String[] args) {
Supplier<String> messageSupplier = () -> "Hello from Supplier!";
Supplier<Integer> randomNumber = () -> new Random().nextInt(100);
Supplier<Double> pi = () -> Math.PI;
Supplier<Long> timestamp = () -> System.currentTimeMillis();
System.out.println(messageSupplier.get());
System.out.println("Random number: " + randomNumber.get());
System.out.println("Another random: " + randomNumber.get());
System.out.println("Pi: " + pi.get());
System.out.println("Timestamp: " + timestamp.get());
}
}Each supplier encapsulates the logic for producing a value. The messageSupplier returns a constant string,
while randomNumber generates a new random integer each time you call get(). Notice how the same supplier can produce
different values on subsequent calls—suppliers don't have to be pure functions, though they can be.
When the value creation requires multiple steps or complex logic, use a block with curly braces and an explicit return statement:
package blog.academy.javapro;
import java.util.function.Supplier;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
public class SupplierWithBlocks {
public static void main(String[] args) {
Supplier<String> formattedTimestamp = () -> {
LocalDateTime now = LocalDateTime.now();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
return now.format(formatter);
};
Supplier<Integer> complexCalculation = () -> {
int base = 10;
int factor = 5;
int result = base * factor;
result += 50;
return result;
};
System.out.println("Formatted time: " + formattedTimestamp.get());
System.out.println("Calculation result: " + complexCalculation.get());
}
}This approach keeps the supplier's implementation encapsulated while handling multi-step value generation. The block structure makes complex logic more readable than cramming everything into a single expression.
Method references work when an existing no-argument method produces the desired type:
package blog.academy.javapro;
import java.util.function.Supplier;
import java.util.UUID;
public class SupplierMethodReference {
private static String generateId() {
return "ID-" + System.nanoTime();
}
private static Double getRandomValue() {
return Math.random() * 100;
}
public static void main(String[] args) {
Supplier<String> uuidSupplier = UUID.randomUUID()::toString;
Supplier<String> idSupplier = SupplierMethodReference::generateId;
Supplier<Double> randomSupplier = SupplierMethodReference::getRandomValue;
Supplier<Double> mathRandom = Math::random;
System.out.println("UUID: " + uuidSupplier.get());
System.out.println("Custom ID: " + idSupplier.get());
System.out.println("Random value: " + randomSupplier.get());
System.out.println("Math.random: " + mathRandom.get());
}
}Method references to static methods like Math::random or instance methods like UUID.randomUUID()::toString create
suppliers concisely. The syntax reads naturally once you recognize the pattern—you're referencing a method that takes no
arguments and returns something, exactly what Supplier requires.
The get() method triggers the supplier to produce its value. Nothing happens until you call get()—the supplier sits
dormant, holding the recipe for creating a value rather than the value itself. This lazy evaluation pattern proves
crucial for performance optimization and resource management:
package blog.academy.javapro;
import java.util.function.Supplier;
public class LazyEvaluation {
private static String expensiveOperation() {
System.out.println("Performing expensive operation...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return "Expensive result";
}
public static void main(String[] args) {
System.out.println("Creating supplier...");
Supplier<String> expensiveSupplier = LazyEvaluation::expensiveOperation;
System.out.println("Supplier created (no expensive work done yet)");
System.out.println("\nNow calling get()...");
String result = expensiveSupplier.get();
System.out.println("Result: " + result);
System.out.println("\nCalling get() again...");
String result2 = expensiveSupplier.get();
System.out.println("Result: " + result2);
}
}The expensive operation doesn't execute when you create the supplier—only when you call get(). Each get() invocation
triggers the operation again, which means suppliers don't cache results by default. They represent the computation, not
its outcome.
This pattern integrates beautifully with Java's Optional class, which uses suppliers for default value generation.
The orElseGet() method accepts a supplier and calls it only if the Optional is empty:
package blog.academy.javapro;
import java.util.Optional;
import java.util.function.Supplier;
public class SupplierWithOptional {
private static String createDefaultValue() {
System.out.println("Creating default value...");
return "DEFAULT";
}
public static void main(String[] args) {
Supplier<String> defaultSupplier = SupplierWithOptional::createDefaultValue;
Optional<String> hasValue = Optional.of("EXISTING");
Optional<String> isEmpty = Optional.empty();
System.out.println("Getting from Optional with value:");
String result1 = hasValue.orElseGet(defaultSupplier);
System.out.println("Result: " + result1);
System.out.println("\nGetting from empty Optional:");
String result2 = isEmpty.orElseGet(defaultSupplier);
System.out.println("Result: " + result2);
}
}When the Optional contains a value, orElseGet() returns it immediately without calling the supplier. Only when the
Optional is empty does it invoke get() on the supplier. This avoids wasting resources creating default values you
might never use. Compare this to orElse(), which evaluates its argument eagerly regardless of whether the Optional has
a value—a subtle but important difference.
Suppliers excel at object creation patterns, effectively serving as factories that produce new instances on demand. This pattern separates object construction from object usage, a fundamental principle in maintainable software design:
package blog.academy.javapro;
import java.util.function.Supplier;
import java.util.ArrayList;
import java.util.List;
public class SupplierFactory {
static class Configuration {
private String environment;
private int maxConnections;
private boolean enableLogging;
public Configuration(String environment, int maxConnections, boolean enableLogging) {
this.environment = environment;
this.maxConnections = maxConnections;
this.enableLogging = enableLogging;
}
@Override
public String toString() {
return String.format("Config[env=%s, maxConn=%d, logging=%b]",
environment, maxConnections, enableLogging);
}
}
public static void main(String[] args) {
Supplier<Configuration> devConfig = () ->
new Configuration("development", 10, true);
Supplier<Configuration> prodConfig = () ->
new Configuration("production", 100, false);
Supplier<List<String>> listFactory = ArrayList::new;
Configuration dev1 = devConfig.get();
Configuration dev2 = devConfig.get();
System.out.println("Dev config 1: " + dev1);
System.out.println("Dev config 2: " + dev2);
System.out.println("Same instance? " + (dev1 == dev2));
Configuration prod = prodConfig.get();
System.out.println("Prod config: " + prod);
List<String> list1 = listFactory.get();
List<String> list2 = listFactory.get();
list1.add("Item 1");
System.out.println("List 1 size: " + list1.size());
System.out.println("List 2 size: " + list2.size());
}
}Each supplier acts as a factory, producing a fresh instance whenever you call get(). The devConfig supplier creates
development configurations, the prodConfig supplier creates production configurations, and the listFactory creates
empty ArrayLists. Notice that the factory pattern naturally emerges from Supplier's signature—no input parameters, one
output value, new instance on each call.
This pattern proves particularly useful when working with streams or other APIs that need factories. The Stream API uses suppliers extensively for generation operations:
package blog.academy.javapro;
import java.util.function.Supplier;
import java.util.stream.Stream;
import java.util.Random;
public class StreamGeneration {
public static void main(String[] args) {
Supplier<Integer> randomInts = () -> new Random().nextInt(100);
Supplier<String> serialNumbers = new Supplier<>() {
private int counter = 1000;
public String get() {
return "SN-" + (counter++);
}
};
System.out.println("Random integers:");
Stream.generate(randomInts)
.limit(5)
.forEach(n -> System.out.println(" " + n));
System.out.println("\nSerial numbers:");
Stream.generate(serialNumbers)
.limit(5)
.forEach(System.out::println);
}
}The Stream.generate() method accepts a supplier and creates an infinite stream by repeatedly calling get(). You
limit the stream to prevent infinite iteration, but the supplier keeps producing values as long as the stream requests
them. The serialNumbers supplier maintains internal state, incrementing a counter with each call—suppliers can be
stateful when the use case demands it.
Suppliers truly shine when dealing with operations that are expensive, time-consuming, or should only execute under certain conditions. By wrapping these operations in suppliers, you defer their execution until the value is actually needed:
package blog.academy.javapro;
import java.util.function.Supplier;
public class DeferredExecution {
static class DatabaseConnection {
public DatabaseConnection() {
System.out.println("Establishing database connection (expensive!)");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
public String query(String sql) {
return "Results for: " + sql;
}
}
private static void processWithEagerConnection(DatabaseConnection conn, boolean needsDatabase) {
System.out.println("Processing with eager connection...");
if (needsDatabase) {
String results = conn.query("SELECT * FROM users");
System.out.println(results);
} else {
System.out.println("Database not needed, but connection already created!");
}
}
private static void processWithLazyConnection(Supplier<DatabaseConnection> connSupplier, boolean needsDatabase) {
System.out.println("Processing with lazy connection...");
if (needsDatabase) {
DatabaseConnection conn = connSupplier.get();
String results = conn.query("SELECT * FROM users");
System.out.println(results);
} else {
System.out.println("Database not needed, connection never created!");
}
}
public static void main(String[] args) {
System.out.println("=== Eager approach ===");
DatabaseConnection eagerConn = new DatabaseConnection();
processWithEagerConnection(eagerConn, false);
System.out.println("\n=== Lazy approach ===");
Supplier<DatabaseConnection> lazyConn = DatabaseConnection::new;
processWithLazyConnection(lazyConn, false);
System.out.println("\n=== Lazy approach when needed ===");
processWithLazyConnection(lazyConn, true);
}
}The eager approach creates the database connection immediately, wasting resources if the connection isn't needed. The lazy approach wraps connection creation in a supplier, establishing the connection only when the code path actually requires it. This pattern appears throughout enterprise applications where resource initialization is expensive and conditional execution is common.
Suppliers also enable thread-safe lazy initialization patterns. While this example shows a simple version, production code often uses suppliers with double-checked locking or other synchronization mechanisms:
package blog.academy.javapro;
import java.util.function.Supplier;
public class LazyInitialization {
static class ExpensiveResource {
public ExpensiveResource() {
System.out.println("Creating expensive resource...");
}
public void use() {
System.out.println("Using resource");
}
}
static class ResourceHolder {
private ExpensiveResource resource;
private Supplier<ExpensiveResource> resourceSupplier;
public ResourceHolder(Supplier<ExpensiveResource> supplier) {
this.resourceSupplier = supplier;
}
public ExpensiveResource getResource() {
if (resource == null) {
System.out.println("Resource not yet initialized, creating...");
resource = resourceSupplier.get();
}
return resource;
}
}
public static void main(String[] args) {
Supplier<ExpensiveResource> supplier = ExpensiveResource::new;
ResourceHolder holder = new ResourceHolder(supplier);
System.out.println("Holder created, resource not yet initialized");
System.out.println("\nFirst access:");
holder.getResource().use();
System.out.println("\nSecond access:");
holder.getResource().use();
}
}The resource holder defers resource creation until the first call to getResource(), then caches the instance for
subsequent calls. The supplier provides the initialization logic without performing it immediately, a pattern that
balances performance with resource management.
Test data generation represents another domain where suppliers excel. Rather than hardcoding test values or building complex test data builders, suppliers provide a functional approach to generating varied, random, or sequential data:
package blog.academy.javapro;
import java.util.function.Supplier;
import java.util.Random;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class TestDataGeneration {
static class User {
private String username;
private String email;
private int age;
public User(String username, String email, int age) {
this.username = username;
this.email = email;
this.age = age;
}
@Override
public String toString() {
return String.format("User[%s, %s, age=%d]", username, email, age);
}
}
public static void main(String[] args) {
Random random = new Random();
Supplier<String> usernameGenerator = () -> {
String[] prefixes = {"user", "admin", "test", "demo"};
String prefix = prefixes[random.nextInt(prefixes.length)];
return prefix + random.nextInt(10000);
};
Supplier<String> emailGenerator = () ->
usernameGenerator.get() + "@example.com";
Supplier<Integer> ageGenerator = () ->
18 + random.nextInt(50);
Supplier<User> userFactory = () ->
new User(usernameGenerator.get(), emailGenerator.get(), ageGenerator.get());
List<User> testUsers = Stream.generate(userFactory)
.limit(5)
.collect(Collectors.toList());
System.out.println("Generated test users:");
testUsers.forEach(System.out::println);
}
}Each supplier generates a different aspect of test data—usernames, emails, ages—while the userFactory supplier
composes them into complete User objects. This compositional approach scales well as test requirements grow. Need
different data distributions? Change the supplier implementations. Need deterministic data for specific tests? Swap in
suppliers that generate predictable sequences.
The pattern extends naturally to more sophisticated scenarios like generating hierarchical data, respecting constraints, or producing data that follows specific business rules:
package blog.academy.javapro;
import java.util.function.Supplier;
import java.time.LocalDate;
import java.time.temporal.ChronoUnit;
import java.util.Random;
public class ComplexDataGeneration {
static class Transaction {
private String id;
private LocalDate date;
private double amount;
private String type;
public Transaction(String id, LocalDate date, double amount, String type) {
this.id = id;
this.date = date;
this.amount = amount;
this.type = type;
}
@Override
public String toString() {
return String.format("Transaction[%s, %s, $%.2f, %s]", id, date, amount, type);
}
}
public static void main(String[] args) {
Random random = new Random();
Supplier<String> transactionId = new Supplier<>() {
private int counter = 1;
public String get() {
return "TXN-" + String.format("%05d", counter++);
}
};
Supplier<LocalDate> recentDate = () -> {
long daysAgo = random.nextInt(90);
return LocalDate.now().minus(daysAgo, ChronoUnit.DAYS);
};
Supplier<Double> amount = () -> {
double base = 10 + (random.nextDouble() * 990);
return Math.round(base * 100.0) / 100.0;
};
Supplier<String> type = () -> {
String[] types = {"DEBIT", "CREDIT", "TRANSFER"};
return types[random.nextInt(types.length)];
};
Supplier<Transaction> transactionSupplier = () ->
new Transaction(transactionId.get(), recentDate.get(), amount.get(), type.get());
System.out.println("Generated transactions:");
for (int i = 0; i < 5; i++) {
System.out.println(transactionSupplier.get());
}
}
}The transactionId supplier maintains state to generate sequential IDs, while other suppliers produce random values
within appropriate ranges. This combination of stateful and stateless suppliers creates realistic test data that
respects real-world constraints—transaction IDs increase sequentially, dates fall within recent history, amounts use
proper currency precision.
The Supplier functional interface provides a clean abstraction for value generation and deferred execution. Its single
method get() produces a value without requiring any input, making it the functional opposite of Consumer and a
complement to Function. This simple signature enables powerful patterns—lazy initialization, factory methods, test data
generation, and conditional computation.
Lambda expressions make creating suppliers syntactically clean. An expression like () -> Math.random() captures the
value generation logic without ceremony, while multi-statement blocks handle complex initialization sequences. Method
references offer even more concise syntax when existing no-argument methods match the supplier contract, with patterns
like ArrayList::new appearing frequently in factory contexts.
The key insight with suppliers is that they represent the potential for creating a value rather than the value itself.
This lazy evaluation pattern avoids wasting resources on computations that might never be needed. When you pass a
supplier to a method, you're giving that method the option to request a value only if and when it actually needs one.
The Optional class leverages this pattern extensively with orElseGet(), and the Stream API uses it for infinite stream
generation with Stream.generate().
Suppliers shine brightest when dealing with expensive operations—database connections, file I/O, complex calculations,
or resource initialization. By deferring these operations until necessary, you improve application performance and
resource utilization. The factory pattern emerges naturally from Supplier's signature, producing fresh instances on
each get() call without requiring explicit factory classes. Whether you're generating test data, implementing lazy
initialization, or building object factories, Supplier provides the functional interface that makes these patterns
type-safe, composable, and elegantly simple.