Supplier Functional Interface in Java

The line below looks harmless, but it hits the database on every call — even when the user is already found:
User user = repository.findById(id).orElse(loadDefaultUser()); The argument of orElse is an ordinary value, so it is always evaluated. Replace it with a Supplier and the extra query disappears: orElseGet(() -> loadDefaultUser()) runs only when the Optional is empty. That deferred execution is exactly why Supplier exists.
Supplier<T> is a built-in functional interface introduced in Java SE 8 as part of the java.util.function package. It takes no arguments and returns an object of type T: it supplies a value on demand.
Signature and function descriptor
The declaration of java.util.function.Supplier is as short as it gets:
@FunctionalInterface
public interface Supplier<T> {
T get();
} Use Supplier when nothing is passed in but a result is expected. The function descriptor of the interface is:
() -> T The single abstract method get() runs from scratch on every call. It may return the same value each time (a constant, a singleton) or a different one (a random number, the current timestamp, a fresh object) — that is decided by the implementation, not by the interface.
Important
A Supplier caches nothing. Creating a Supplier does not start any computation — the body of the lambda executes only when get() is called, and it executes as many times as you call it.
Supplier examples in Java
The simplest Supplier example is a lambda that returns a string in upper case:
import java.util.function.Supplier;
public class SupplierExample {
public static void main(String[] args) {
String t = "One";
Supplier<String> supplierStr = () -> t.toUpperCase();
System.out.println(supplierStr.get()); // ONE
}
} The second common form is a constructor reference: a no-argument constructor fits the () -> T descriptor perfectly.
Supplier<List<String>> listFactory = ArrayList::new;
List<String> first = listFactory.get();
List<String> second = listFactory.get();
System.out.println(first == second); // false - every call creates a new list The third form is a source of a fresh value on every call:
Supplier<Double> random = Math::random;
Supplier<LocalDateTime> now = LocalDateTime::now;
System.out.println(random.get()); // 0.734...
System.out.println(random.get()); // 0.118... - a different value already And the fourth, the practical one: a Supplier passed as a method parameter, so that the caller decides which object gets created. In the snippet below a class Bird extends Animal and overrides its move() method, while moveAnimal() only knows that it will be handed a supplier of animals:
public class Animal {
public void move() {
System.out.println("Move");
}
}
public class Bird extends Animal {
@Override
public void move() {
System.out.println("Fly");
}
}
public class TestAnimal {
public static void main(String[] args) {
moveAnimal(() -> new Animal()); // Move
moveAnimal(() -> new Bird()); // Fly
}
public static void moveAnimal(Supplier<Animal> supplier) {
Animal animal = supplier.get();
animal.move();
}
} Because both lambdas do nothing but call a no-argument constructor, they collapse into method references:
public static void main(String[] args) {
moveAnimal(Animal::new);
moveAnimal(Bird::new);
} The same idea generalises into a tiny factory helper:
public static <T> List<T> repeat(int count, Supplier<T> factory) {
List<T> result = new ArrayList<>();
for (int i = 0; i < count; i++) {
result.add(factory.get());
}
return result;
}
List<StringBuilder> builders = repeat(3, StringBuilder::new); Lazy evaluation: the main use case
Supplier earns its keep where a value may never be needed and computing it is expensive. The method receives not a ready object but a recipe for producing one, and calls get() only if it has to.
Optional<User> found = repository.findById(id);
// bad: createGuest() runs every time, even when the user was found
User a = found.orElse(createGuest());
// good: createGuest() runs only for an empty Optional
User b = found.orElseGet(() -> createGuest());
// the exception object is built only when it is actually thrown
User c = found.orElseThrow(() -> new UserNotFoundException(id)); The same trick shows up in logging: the string concatenation never happens if the log level is disabled.
// java.util.logging, Java 8+
logger.fine(() -> "Report: " + buildExpensiveReport()); Since Java 9 the standard library also offers Objects.requireNonNullElseGet(obj, supplier) — the same idea applied to default values.
Where Supplier appears in the JDK
Even if you never declare a Supplier yourself, you pass one into standard library methods all the time:
Optional.orElseGet(Supplier),Optional.orElseThrow(Supplier),Optional.or(Supplier);Stream.generate(Supplier)— produces an infinite stream, so it must always be bounded withlimit();Collectors.toCollection(Supplier)— when you need a specific collection type;CompletableFuture.supplyAsync(Supplier)— computing a result asynchronously;ThreadLocal.withInitial(Supplier)— the initial value for each thread.
List<String> ids = Stream.generate(() -> UUID.randomUUID().toString())
.limit(3)
.collect(Collectors.toList());
TreeSet<String> sorted = names.stream()
.collect(Collectors.toCollection(TreeSet::new)); Primitive Supplier variants
To avoid autoboxing, java.util.function ships specialised suppliers of primitives. Each of them has its own method — not get().
| Interface | Method | Returns | When to use it |
|---|---|---|---|
| Supplier<T> | T get() | An object of any type | The general case |
| IntSupplier | int getAsInt() | int | Counters, indexes, number generation |
| LongSupplier | long getAsLong() | long | Timestamps, large counters |
| DoubleSupplier | double getAsDouble() | double | Random and floating-point values |
| BooleanSupplier | boolean getAsBoolean() | boolean | A deferred condition check, for example in waits and retries |
Note
Unlike Function with its andThen() and compose(), Supplier has no default methods at all — only get(). Two suppliers cannot be chained out of the box; you write the composition by hand: () -> mapper.apply(source.get()).
Supplier vs Function, Consumer and Callable
The functional interfaces in java.util.function differ by the shape of their descriptor: how many arguments they accept and what they return.
| Interface | Method | Descriptor | Meaning |
|---|---|---|---|
| Supplier<T> | T get() | () → T | Takes nothing, returns a value |
| Consumer<T> | void accept(T t) | T → void | Takes a value, returns nothing |
| Function<T, R> | R apply(T t) | T → R | Transforms one value into another |
| Predicate<T> | boolean test(T t) | T → boolean | Checks a condition |
| Callable<V> | V call() throws Exception | () → V | Same shape as Supplier, but may throw checked exceptions |
| Runnable | void run() | () → void | An action with no input and no result |
The interview favourite is the Supplier / Callable pair: the signatures look almost identical, but Callable.call() declares throws Exception while Supplier.get() does not. Hence the different homes and purposes — Callable lives in java.util.concurrent and is meant for tasks submitted to an ExecutorService, whereas Supplier is meant for obtaining a value lazily.
Where developers get tripped up
- Mixing up
orElseandorElseGet.orElsetakes an already computed value and evaluates it unconditionally;orElseGettakes a Supplier and calls it only for an emptyOptional. - Expecting caching. Every
get()runs the lambda body again. If the result must be computed once, you have to memoize it yourself. - Trying to throw a checked exception from the lambda body — the code will not compile, because
get()declares nothrows. - Forgetting
limit()afterStream.generate(...)— the stream is infinite and the terminal operation hangs. - Using
Supplier<Integer>in hot code instead ofIntSupplier, paying for boxing on every loop iteration.
A memoizing wrapper, for when the value must be computed exactly once:
public static <T> Supplier<T> memoize(Supplier<T> delegate) {
Map<String, T> cache = new ConcurrentHashMap<>();
return () -> cache.computeIfAbsent("value", k -> delegate.get());
} Tip
Variables captured by a Supplier lambda must be effectively final. If you need to supply changing state, capture an object instead of the variable — a field, or an AtomicInteger — and read it inside get().
The full contract is described in the official Oracle documentation.
Frequently asked questions
What is a Supplier in Java in simple terms?
Supplier is a functional interface from the java.util.function package, added in Java 8. It has a single method T get() that takes no arguments and returns a value. In plain words, it is a supplier of an object that is called only when the object is actually needed.
What is the difference between orElse and orElseGet?
orElse(value) receives an already computed value, so the expression inside the parentheses runs every time — even when the Optional is not empty. orElseGet(supplier) receives a Supplier and calls get() only for an empty Optional. When the fallback is expensive, such as a database query or object creation, orElseGet is the one you want.
Is there a Supplier that returns void in Java?
No. A supplier exists to produce a result, so a void version would be meaningless. For an action that takes nothing and returns nothing, use Runnable with its run() method; for a primitive result, use IntSupplier, LongSupplier, DoubleSupplier or BooleanSupplier instead of boxing into Supplier.
Can a Supplier throw a checked exception?
No. The get() method declares no throws clause, so a checked exception inside the lambda will not compile. The options are to wrap it into an unchecked one such as UncheckedIOException, to use Callable whose call() is declared as throws Exception, or to define your own functional interface with a throws clause.
Does Supplier cache the result of get()?
No, there is no caching: the lambda body runs on every call to get(). If the value must be computed once, write a memoizing wrapper based on ConcurrentHashMap.computeIfAbsent, or use the ready-made Suppliers.memoize from Guava.
Комментарии