Java Tutorial
🔍

Java Supplier Interface

Java Supplier Interface

Supplier<T> is the functional interface for producing a value with no input at all — it declares one abstract method, get(), and exists for exactly one reason: to delay when a value actually gets created until the exact moment it is genuinely needed. Function transforms a value you already have, Consumer acts on one, and Predicate tests one, but Supplier is the only one of the four core java.util.function interfaces that starts from nothing and hands something back.

What Is Supplier?

Supplier<T> declares T get() as its single abstract method, making it a valid target for any lambda, method reference, or constructor reference that takes no arguments and returns a value. Unlike Predicate, Function, and Consumer, it has no default methods at all — there is nothing to combine, since a supplier's whole job is producing exactly one value.

It shows up wherever a fallback or a new object should only be built if it is actually going to be used — Optional.orElseGet, Objects.requireNonNullElseGet, factory methods, and Stream.generate are the places you will run into it most.

Why Supplier Was Introduced

A plain method call as an argument runs the moment the surrounding expression is evaluated, whether or not its result ends up being used. Optional.orElse takes exactly this kind of argument, and it evaluates it every single time, even when the Optional already has a value and the fallback is thrown away immediately.

1// File: BeforeSupplier.java 2import java.util.*; 3 4public class BeforeSupplier { 5 static String loadFromCache(String key) { 6 System.out.println("Checking cache for " + key); 7 return "cached-session-42"; 8 } 9 10 static String buildExpensiveDefault() { 11 System.out.println("Building expensive default session"); 12 return "DEFAULT_SESSION"; 13 } 14 15 public static void main(String[] args) { 16 String cached = loadFromCache("user-42"); 17 18 // orElse evaluates its argument immediately, even when cached is 19 // already present - buildExpensiveDefault() runs regardless 20 String session = Optional.ofNullable(cached).orElse(buildExpensiveDefault()); 21 System.out.println("Session: " + session); 22 } 23}
Output:
Checking cache for user-42
Building expensive default session
Session: cached-session-42

buildExpensiveDefault() ran and printed its message even though cached was already present and its result was never used. Passing a Supplier instead of an already-computed value defers that call entirely, running it only when the fallback is genuinely needed.

1// File: AfterSupplier.java 2import java.util.*; 3import java.util.function.*; 4 5public class AfterSupplier { 6 static String loadFromCache(String key) { 7 System.out.println("Checking cache for " + key); 8 return "cached-session-42"; 9 } 10 11 static String buildExpensiveDefault() { 12 System.out.println("Building expensive default session"); 13 return "DEFAULT_SESSION"; 14 } 15 16 public static void main(String[] args) { 17 String cached = loadFromCache("user-42"); 18 19 Supplier<String> defaultSessionSupplier = AfterSupplier::buildExpensiveDefault; 20 21 // orElseGet only calls the Supplier when cached is actually absent 22 String session = Optional.ofNullable(cached).orElseGet(defaultSessionSupplier); 23 System.out.println("Session: " + session); 24 } 25}
Output:
Checking cache for user-42
Session: cached-session-42

buildExpensiveDefault never runs this time, because orElseGet only calls get() on the Supplier when the Optional is actually empty.

Syntax

get() produces the value, and a Supplier can wrap a lambda, an existing method through a reference, or even a constructor.

1// File: SupplierSyntaxForms.java 2import java.util.*; 3import java.util.function.*; 4 5public class SupplierSyntaxForms { 6 public static void main(String[] args) { 7 Supplier<String> greeting = () -> "Welcome back"; 8 Supplier<ArrayList<String>> newSessionList = ArrayList::new; 9 Supplier<Double> randomScore = Math::random; 10 11 System.out.println("get(): " + greeting.get()); 12 System.out.println("Constructor reference produces empty list: " + newSessionList.get().isEmpty()); 13 System.out.println("Math::random is in range 0-1: " + (randomScore.get() < 1.0)); 14 } 15}
Output:
get(): Welcome back
Constructor reference produces empty list: true
Math::random is in range 0-1: true

Common Use Cases

Supplying a Fallback Only When Needed

Objects.requireNonNullElseGet follows the same lazy pattern as Optional.orElseGet, computing a fallback only if the original value is actually null.

1// File: RequireNonNullElseGetExample.java 2import java.util.*; 3 4public class RequireNonNullElseGetExample { 5 public static void main(String[] args) { 6 String userNickname = null; 7 8 String resolved = Objects.requireNonNullElseGet(userNickname, () -> "Guest"); 9 System.out.println("Resolved nickname: " + resolved); 10 } 11}
Output:
Resolved nickname: Guest

Acting as an Object Factory

A Supplier stored as a field or passed as a parameter turns object creation itself into a swappable, reusable value, instead of a new expression fixed at one specific call site.

1// File: SessionFactoryExample.java 2import java.util.*; 3import java.util.function.*; 4 5public class SessionFactoryExample { 6 record Session(String sessionId, long createdAt) {} 7 8 public static void main(String[] args) { 9 Supplier<Session> sessionFactory = () -> new Session(UUID.randomUUID().toString().substring(0, 8), 0L); 10 11 Session sessionOne = sessionFactory.get(); 12 Session sessionTwo = sessionFactory.get(); 13 14 System.out.println("Same session id: " + sessionOne.sessionId().equals(sessionTwo.sessionId())); 15 } 16}
Output:
Same session id: false

Deferring an Expensive Computation

Passing a Supplier into a logging method means the expensive part of building a debug message only runs when that message will actually be printed.

1// File: LazyLoggingExample.java 2import java.util.function.*; 3 4public class LazyLoggingExample { 5 static void logDebug(boolean debugEnabled, Supplier<String> messageSupplier) { 6 if (debugEnabled) { 7 System.out.println("DEBUG: " + messageSupplier.get()); 8 } 9 } 10 11 static String buildExpensiveMessage() { 12 System.out.println("Building expensive debug message"); 13 return "Session state dump complete"; 14 } 15 16 public static void main(String[] args) { 17 logDebug(false, LazyLoggingExample::buildExpensiveMessage); 18 logDebug(true, LazyLoggingExample::buildExpensiveMessage); 19 } 20}
Output:
Building expensive debug message
DEBUG: Session state dump complete

Generating a Bounded Stream of Values

Stream.generate calls a Supplier repeatedly to build a stream, which only makes sense combined with limit, since the stream it produces has no natural end on its own.

1// File: StreamGenerateSupplierExample.java 2import java.util.*; 3import java.util.function.*; 4import java.util.stream.*; 5 6public class StreamGenerateSupplierExample { 7 public static void main(String[] args) { 8 int[] counter = {0}; 9 Supplier<Integer> nextId = () -> ++counter[0]; 10 11 List<Integer> generatedIds = Stream.generate(nextId) 12 .limit(4) 13 .collect(Collectors.toList()); 14 15 System.out.println(generatedIds); 16 } 17}
Output:
[1, 2, 3, 4]

Real-World Example

A backend service handling user sessions typically wants to reuse a session from cache whenever one already exists, and only pay the real cost of building a new one — generating a token, writing to a datastore, whatever that construction actually involves — when the cache genuinely has nothing for that user. Accepting a Supplier<UserSession> in the cache lookup method means that construction logic never runs on a cache hit, no matter how the caller happens to build it.

1// File: UserSession.java 2 3public class UserSession { 4 private final String sessionId; 5 private final String userId; 6 7 public UserSession(String sessionId, String userId) { 8 this.sessionId = sessionId; 9 this.userId = userId; 10 } 11 12 @Override 13 public String toString() { 14 return "UserSession(" + sessionId + ", " + userId + ")"; 15 } 16}
1// File: SessionCache.java 2import java.util.*; 3import java.util.function.*; 4 5public class SessionCache { 6 private final Map<String, UserSession> sessions = new HashMap<>(); 7 8 public UserSession getOrCreate(String userId, Supplier<UserSession> factory) { 9 UserSession existing = sessions.get(userId); 10 if (existing != null) { 11 System.out.println("Cache hit for " + userId); 12 return existing; 13 } 14 15 System.out.println("Cache miss for " + userId + " - creating a new session"); 16 UserSession created = factory.get(); 17 sessions.put(userId, created); 18 return created; 19 } 20}
1// File: SessionCacheDemo.java 2 3public class SessionCacheDemo { 4 static int sessionsCreated = 0; 5 6 static UserSession buildSession(String userId) { 7 sessionsCreated++; 8 String sessionId = "SESSION-" + sessionsCreated; 9 return new UserSession(sessionId, userId); 10 } 11 12 public static void main(String[] args) { 13 SessionCache cache = new SessionCache(); 14 15 UserSession first = cache.getOrCreate("user-42", () -> buildSession("user-42")); 16 UserSession second = cache.getOrCreate("user-42", () -> buildSession("user-42")); 17 UserSession third = cache.getOrCreate("user-99", () -> buildSession("user-99")); 18 19 System.out.println("first: " + first); 20 System.out.println("second: " + second); 21 System.out.println("third: " + third); 22 System.out.println("Sessions actually created: " + sessionsCreated); 23 } 24}
Output:
Cache miss for user-42 - creating a new session
Cache hit for user-42
Cache miss for user-99 - creating a new session
first: UserSession(SESSION-1, user-42)
second: UserSession(SESSION-1, user-42)
third: UserSession(SESSION-2, user-99)
Sessions actually created: 2

Three getOrCreate calls happen, but sessionsCreated only reaches 2. The second call for user-42 passes a lambda that would build yet another session, but factory.get() never runs, because getOrCreate finds an existing session in the cache first. During code reviews, seniors commonly flag a getOrCreate signature that accepts a UserSession directly instead of a Supplier<UserSession>, because that forces the caller to build the session eagerly at the call site regardless of whether the cache actually needs it — passing the factory itself is exactly what keeps buildSession from running on every cache hit.

Combining Supplier With Other Features

Supplier pairs naturally with Optional.orElseGet and Objects.requireNonNullElseGet for lazy fallbacks, and with Stream.generate for producing a stream of values on demand. It is worth keeping distinct from Function, however: Map.computeIfAbsent looks similar at first glance, but its mapping argument is a Function<K, V>, not a Supplier<V>, because it needs the missing key handed to it to build the value — a Supplier simply has nowhere to receive that key.

Best Practices

Reach for Supplier any time a default or fallback value is even slightly expensive to build, instead of computing it eagerly and discarding the result most of the time. orElseGet, requireNonNullElseGet, and any custom method following the same pattern all exist specifically for this.

Keep a Supplier genuinely parameterless by design. If the value being produced actually depends on some input, the correct interface is Function, not a Supplier quietly capturing an outside variable to stand in for what should have been a real parameter.

Prefer orElseGet over orElse whenever the fallback is not a cheap, already-existing constant. orElse evaluates its argument unconditionally, so the moment the fallback involves any real work, orElseGet is the only version that actually defers it.

Common Mistakes

Passing an expensive method call directly to orElse is one of the most common Supplier-adjacent mistakes, precisely because it does not look wrong at first glance.

1// File: OrElseEagerMistake.java 2import java.util.*; 3 4public class OrElseEagerMistake { 5 static int expensiveCallCount = 0; 6 7 static String expensiveDefault() { 8 expensiveCallCount++; 9 return "DEFAULT"; 10 } 11 12 public static void main(String[] args) { 13 Optional<String> present = Optional.of("actual-value"); 14 15 // orElse evaluates expensiveDefault() immediately, even though 16 // present already has a value and the default is never used 17 String resultWithOrElse = present.orElse(expensiveDefault()); 18 System.out.println("orElse result: " + resultWithOrElse + ", calls: " + expensiveCallCount); 19 20 String resultWithOrElseGet = present.orElseGet(OrElseEagerMistake::expensiveDefault); 21 System.out.println("orElseGet result: " + resultWithOrElseGet + ", calls: " + expensiveCallCount); 22 } 23}
Output:
orElse result: actual-value, calls: 1
orElseGet result: actual-value, calls: 1

Assuming a Supplier caches its result after the first get() call is another frequent misconception. Nothing about Supplier memoizes anything automatically — every call to get() runs the underlying logic again from scratch.

1// File: SupplierNoCachingMistake.java 2import java.util.function.*; 3 4public class SupplierNoCachingMistake { 5 static int callCount = 0; 6 7 public static void main(String[] args) { 8 Supplier<Integer> counter = () -> ++callCount; 9 10 System.out.println("First get(): " + counter.get()); 11 System.out.println("Second get(): " + counter.get()); 12 System.out.println("Third get(): " + counter.get()); 13 } 14}
Output:
First get(): 1
Second get(): 2
Third get(): 3

Treating a single .get() call's result as if it represents ongoing, live state is a subtler version of the same mistake. A Supplier produces one fresh value each time it runs — reusing an old result instead of calling get() again means silently working with stale data the moment the underlying state has actually changed.

Interview Questions

Q1. What is the Supplier interface in Java, and what makes it different from Function, Consumer, and Predicate?

Supplier<T> produces a value with no input at all, declaring T get() as its single abstract method. Function transforms an input into an output, Consumer acts on an input and returns nothing, and Predicate tests an input and returns a boolean — Supplier is the only one of the four that starts with nothing and still hands back a result. Interviewers often ask this early to confirm a candidate can distinguish all four java.util.function interfaces by their actual method signatures rather than by rough description.

Q2. What is the difference between Optional.orElse and Optional.orElseGet?

orElse takes an already-computed value as its argument, which Java evaluates immediately as part of the method call regardless of whether the Optional actually needs it. orElseGet takes a Supplier instead, and only calls get() on it if the Optional is genuinely empty. The practical consequence is that orElse(buildExpensiveDefault()) always runs buildExpensiveDefault(), while orElseGet(this::buildExpensiveDefault) only runs it when the fallback value is actually going to be used.

Q3. Does a Supplier cache or memoize the value it produces?

No, and this is a common misconception. Every call to get() executes the supplier's logic again from the beginning — nothing about the Supplier interface stores or reuses a previous result automatically. Memoization, if it is genuinely needed, has to be built explicitly, typically by wrapping the supplier's logic around a cached field that only computes once.

Q4. How is Supplier used with Stream.generate?

Stream.generate(Supplier<T> supplier) builds a stream by calling the supplier repeatedly, once per element the stream produces, with no natural end point of its own. It only becomes practical when combined with a bounding operation like limit, since consuming an unbounded stream directly would call the supplier forever.

Q5. Why does Supplier have no andThen or compose method, unlike Function?

Because andThen and compose both exist to chain a return value from one function into the input of another, and a Supplier has no input to receive anything into — it only ever produces a result. There is nothing meaningful to chain before a Supplier, since it does not accept a value to begin with.

Q6. When would you choose Supplier over passing an already-computed value as a parameter?

Whenever the value might not actually be needed, or is expensive enough that computing it unconditionally would waste real work. A method that only sometimes needs a fallback, a default, or a newly constructed object should accept a Supplier so that construction happens exactly when required, rather than every time the method is called regardless of whether the result gets used.

FAQs

Can a Supplier take any input at all?

No. Supplier<T> declares get() with zero parameters, by design. If a computation genuinely needs an input, it belongs in a Function, not a Supplier with a captured variable pretending to be a parameter.

Does Supplier support primitive return types without boxing?

Not through Supplier<T> directly, since its type parameter must be a reference type. IntSupplier, LongSupplier, DoubleSupplier, and BooleanSupplier exist specifically to return primitive values without the cost of autoboxing.

Is a constructor reference a valid Supplier?

Yes, as long as the constructor takes no arguments. ArrayList::new is a valid Supplier<ArrayList<T>>, since calling a no-arg constructor and calling get() on a Supplier are both zero-argument operations that produce a new object.

What is the difference between Supplier and Callable?

Both declare a single method that takes no arguments and returns a value, but Callable<T>'s call() method is allowed to throw a checked exception, while Supplier<T>'s get() is not. Callable comes from the concurrency API and is typically used with an ExecutorService, while Supplier belongs to java.util.function and is used throughout the rest of the functional programming style covered in this series.

Can Supplier be used for lazy singleton initialization?

Yes, and this is one of its most common real uses. Wrapping the construction of an expensive singleton in a Supplier and calling get() only the first time it is requested is a common pattern for deferring startup cost until the object is actually needed, often combined with a cached field to avoid rebuilding it on every subsequent call.

Why does Objects.requireNonNullElseGet exist if orElseGet already does something similar?

orElseGet is a method on Optional, so it only applies to values already wrapped in one. Objects.requireNonNullElseGet works directly on a plain, possibly-null reference without requiring it to be wrapped in an Optional first, which is useful in code that has not adopted Optional for a particular value but still wants the same lazy-fallback behavior.

Can a Supplier throw a checked exception?

Not directly. The get() method declares no checked exceptions, so a lambda assigned to Supplier has to catch any checked exception internally or rethrow it as an unchecked one — the same restriction that applies to every interface in java.util.function.

Summary

Supplier<T> exists for one specific and genuinely useful reason: it lets code describe how to produce a value without actually producing it until that value is needed. orElseGet over orElse, a factory passed into a cache lookup instead of a value built eagerly at the call site, a debug message that only gets constructed when debug logging is actually on — all three examples in this article are the same pattern wearing different clothes.

The two things worth carrying forward are that a Supplier never caches anything on its own, so calling get() twice runs the logic twice, and that reaching for Supplier the moment a fallback or a constructed object might go unused is what turns a wasteful default into a genuinely lazy one. With Predicate, Function, Consumer, and Supplier all in place, the rest of java.util.function stops looking like a list of interfaces to memorize and starts looking like four answers to four different, everyday questions.

What to Read Next