Supplier
Supplier<T> is a functional interface in java.util.function that represents a factory or source of values — it takes no arguments and returns a value of type T. The single abstract method is T get(). Unlike Function which transforms an input, Supplier produces a value from thin air — from a constant, a computation, a database query, object construction, or any other source. Supplier is the standard abstraction for lazy evaluation (deferring a computation until its result is actually needed), factory methods (providing fresh instances on demand), and dependency injection (providing values to code that doesn't need to know how they were produced). The primitive specializations BooleanSupplier, IntSupplier, LongSupplier, and DoubleSupplier avoid boxing overhead for primitive return types. Supplier is used throughout the JDK: Optional.orElseGet(Supplier), Objects.requireNonNullElseGet(Supplier), Stream.generate(Supplier), Collectors.toCollection(Supplier), and more. This entry covers the Supplier contract and its distinction from other functional interfaces, all primitive specializations, lazy initialization patterns, the distinction between Supplier and Callable, and canonical usage in Optional and Stream.
Supplier<T> — get() and Lazy Evaluation
// ── Basic Supplier lambda ─────────────────────────────────────────────
import java.util.function.*;
Supplier<String> greeting = () -> "Hello, World!";
Supplier<List<String>> listFactory = () -> new ArrayList<>();
Supplier<Double> random = Math::random; // method reference as Supplier
Supplier<LocalDate> today = LocalDate::now; // static method reference
System.out.println(greeting.get()); // Hello, World!
System.out.println(listFactory.get()); // [] — new ArrayList on each call
System.out.println(random.get()); // random double, different each call
System.out.println(today.get()); // today's date
// ── Factory Supplier: new instance on each get() ──────────────────────
Supplier<StringBuilder> sbFactory = StringBuilder::new; // constructor reference
StringBuilder sb1 = sbFactory.get(); // new StringBuilder
StringBuilder sb2 = sbFactory.get(); // another new StringBuilder
System.out.println(sb1 == sb2); // false — different instances
// ── Constant Supplier: same value every time ──────────────────────────
String sharedValue = "shared";
Supplier<String> constant = () -> sharedValue;
System.out.println(constant.get() == constant.get()); // true — same object
// ── Lazy computation: deferred until get() is called ──────────────────
Supplier<String> expensive = () -> {
System.out.println("Computing...");
return performExpensiveComputation(); // only runs when get() is called
};
// At this point, "Computing..." has NOT been printed
System.out.println("Before get()");
String value = expensive.get(); // NOW the computation runs
System.out.println("Got: " + value);
// Before get()
// Computing...
// Got: <result>
// ── Memoizing Supplier: compute once, cache thereafter ────────────────
public class Lazy<T> {
private final Supplier<T> delegate;
private volatile T cached;
private volatile boolean initialized = false;
public Lazy(Supplier<T> delegate) { this.delegate = delegate; }
public T get() {
if (!initialized) {
synchronized (this) {
if (!initialized) {
cached = delegate.get();
initialized = true;
}
}
}
return cached;
}
}
Lazy<ExpensiveObject> lazyObj = new Lazy<>(() -> new ExpensiveObject());
// ExpensiveObject not created yet
ExpensiveObject obj1 = lazyObj.get(); // created here
ExpensiveObject obj2 = lazyObj.get(); // returned from cache
System.out.println(obj1 == obj2); // true — same instance
// ── Supplier vs value argument: lazy vs eager ─────────────────────────
// Eager: errorMessage() is ALWAYS evaluated, even if no error:
void logIfError(boolean error, String message) { if (error) log(message); }
logIfError(false, expensiveMessage()); // expensiveMessage() runs even though error=false
// Lazy: supplier only evaluated when actually needed:
void logIfError(boolean error, Supplier<String> message) { if (error) log(message.get()); }
logIfError(false, () -> expensiveMessage()); // expensiveMessage() NOT calledPrimitive Specializations and Standard Library Usage
// ── Primitive Suppliers: no boxing ───────────────────────────────────
IntSupplier counter = new AtomicInteger(0)::getAndIncrement; // method ref
LongSupplier clock = System::currentTimeMillis;
DoubleSupplier pi = () -> Math.PI;
BooleanSupplier hasMore = queue::isEmpty;
System.out.println(counter.getAsInt()); // 0
System.out.println(counter.getAsInt()); // 1
System.out.println(clock.getAsLong()); // current time millis
System.out.println(pi.getAsDouble()); // 3.141592653589793
// ── Optional.orElseGet vs orElse: lazy vs eager ───────────────────────
Optional<String> present = Optional.of("Alice");
Optional<String> empty = Optional.empty();
// orElse: default ALWAYS evaluated:
String r1 = present.orElse(expensiveDefault()); // expensiveDefault() RUNS — wasteful
String r2 = empty.orElse(expensiveDefault()); // expensiveDefault() RUNS — needed
// orElseGet: default evaluated ONLY when Optional is empty:
String r3 = present.orElseGet(() -> expensiveDefault()); // NOT called — Alice returned
String r4 = empty.orElseGet(() -> expensiveDefault()); // called — needed
// Rule: prefer orElseGet for non-trivial defaults:
Optional<User> user = userCache.get(id);
String email = user
.map(User::email)
.orElseGet(() -> database.fetchUserEmail(id)); // DB query only if not cached
// ── Stream.generate: infinite stream from Supplier ────────────────────
// Random numbers:
Stream.generate(Math::random)
.limit(5)
.forEach(d -> System.out.printf("%.4f%n", d));
// Unique IDs:
AtomicLong idGen = new AtomicLong(1000);
Stream.generate(idGen::getAndIncrement)
.limit(5)
.forEach(System.out::println); // 1000, 1001, 1002, 1003, 1004
// Default object instances:
Stream.generate(HashMap<String, Integer>::new)
.limit(3)
.forEach(map -> map.put("key", 42)); // three separate HashMaps
// ── Collectors.toCollection: specific collection type ─────────────────
List<String> names = List.of("Charlie", "Alice", "Bob");
// sorted TreeSet via Supplier:
TreeSet<String> sorted = names.stream()
.collect(Collectors.toCollection(TreeSet::new));
System.out.println(sorted); // [Alice, Bob, Charlie]
// LinkedList via Supplier:
LinkedList<String> linked = names.stream()
.collect(Collectors.toCollection(LinkedList::new));
// PriorityQueue via Supplier:
PriorityQueue<String> pq = names.stream()
.collect(Collectors.toCollection(PriorityQueue::new));
// ── Supplier for dependency injection and configuration ───────────────
public class ReportGenerator {
private final Supplier<Connection> connectionSupplier;
// Dependency injected as Supplier — caller controls how connection is obtained:
public ReportGenerator(Supplier<Connection> connectionSupplier) {
this.connectionSupplier = connectionSupplier;
}
public Report generate() {
try (Connection conn = connectionSupplier.get()) { // get fresh connection
return queryAndBuild(conn);
}
}
}
// Test with in-memory connection:
ReportGenerator testGen = new ReportGenerator(() -> h2Connection);
// Production with pool:
ReportGenerator prodGen = new ReportGenerator(() -> connectionPool.acquire());
// ── Supplier vs Callable: checked exceptions ──────────────────────────
// Supplier<T>: get() does NOT declare checked exceptions
// Callable<V>: call() DOES declare throws Exception
Supplier<String> supplier = () -> {
try {
return Files.readString(Path.of("file.txt")); // IOException must be caught
} catch (IOException e) {
throw new UncheckedIOException(e); // wrap checked as unchecked
}
};
Callable<String> callable = () -> Files.readString(Path.of("file.txt"));
// callable.call() can throw IOException — caller handles it
// Use Callable when the caller needs to handle checked exceptions