☕ Java

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

Supplier<T> is declared as @FunctionalInterface with the single abstract method T get(). There is no andThen, compose, or negate — Supplier has no default composition methods. Its sole purpose is to produce a value on demand. The value can be the same object every time (a constant supplier), a new object each time (a factory supplier), or a computationally expensive value deferred until needed (a lazy supplier). The defining property of Supplier is laziness by nature. Passing a Supplier to a method defers the computation until get() is called. This is in contrast to passing the value itself — a value is computed eagerly (before the call), while a Supplier is a thunk (a deferred computation). The method receiving the Supplier decides whether and when to call get(). If the value is not needed (a conditional branch is not taken, an Optional is already present, a cache hit occurs), get() is never called and the computation is never performed. Supplier imposes no contract on get() beyond returning T. A Supplier may: return null (though many APIs that accept Supplier document that null is not permitted), throw an unchecked exception, return the same value on every call, return a different value on every call, or have side effects. When a Supplier should be able to throw a checked exception, the standard Supplier<T> cannot be used directly — the lambda would need to catch the checked exception internally. The java.util.concurrent.Callable<V> interface serves the same purpose but allows checked exceptions from its call() method. Common Supplier patterns: () -> new SomeObject() (factory — new instance on each get()), () -> CONSTANT (constant — same object on each get()), () -> expensiveCompute() (deferred computation — computed once when get() is called), () -> { if (cached == null) cached = compute(); return cached; } (memoizing supplier — computed once, cached thereafter).
Java
// ── 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 called

Primitive Specializations and Standard Library Usage

The four primitive Supplier specializations avoid boxing overhead for their return types. BooleanSupplier returns boolean from getAsBoolean(). IntSupplier returns int from getAsInt(). LongSupplier returns long from getAsLong(). DoubleSupplier returns double from getAsDouble(). These are used when the produced value is a primitive and boxing would add unnecessary allocation overhead — particularly in hot paths or numerical computation. Optional.orElseGet(Supplier<? extends T> supplier) is the canonical Supplier usage in the JDK. It calls supplier.get() only if the Optional is empty — the supplier provides the fallback value lazily. This is in contrast to Optional.orElse(T other), which evaluates other eagerly even if the Optional is present. For computationally expensive or side-effectful defaults (database queries, network calls, object construction), orElseGet is always the correct choice. Stream.generate(Supplier<? extends T> s) creates an infinite sequential stream where each element is provided by s.get(). The stream is lazy — elements are generated only when the terminal operation requests them. Combined with Stream.limit(), it provides a concise way to create streams of any size. Common uses: Stream.generate(() -> new Object()) for infinite streams of new objects, Stream.generate(Math::random) for random number streams, Stream.generate(Queue::poll) to drain a queue as a stream. Collectors.toCollection(Supplier<C> collectionFactory) allows specifying a custom collection implementation as the target of a collect() operation. Instead of Collectors.toList() (which may produce any List implementation), Collectors.toCollection(() -> new LinkedList<>()) guarantees the specific collection type. Objects.requireNonNullElseGet(T obj, Supplier<? extends T> supplier) (Java 9+) returns obj if non-null, otherwise calls supplier.get(). Like Optional.orElseGet, the supplier is only evaluated when obj is null.
Java
// ── 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

Related Topics in Java 8 Features

Intermediate Operations
Intermediate operations are Stream API methods that transform one stream into another stream, enabling pipeline construction through method chaining. Every intermediate operation is lazy — it does not process any elements when called; it only builds a description of the computation to be performed. The actual processing occurs only when a terminal operation triggers stream traversal, at which point all intermediate operations execute in a single pass over the data, interleaved element by element rather than stage by stage. This laziness and fusion model is the defining architectural feature of the Streams API, distinguishing it from eager collection-transformation approaches. Intermediate operations fall into categories: filtering (filter, distinct), transformation (map and its primitive variants, flatMap), ordering (sorted), size-limiting (limit, skip), peeking (peek), and the Java 9 additions for prefix-based selection (takeWhile, dropWhile). This entry covers the laziness model and why it matters, every intermediate operation with its exact semantics and performance characteristics, stateless versus stateful intermediate operations, short-circuiting operations and how they interact with infinite streams, the map/flatMap distinction, and operation ordering and its performance implications.
Terminal Operations
Terminal operations are Stream API methods that trigger the actual traversal and processing of a stream pipeline, producing a non-stream result — a value, a collection, a side effect, or nothing (void). A stream can have exactly one terminal operation; once invoked, the stream is consumed and cannot be reused (calling any further operation on it throws IllegalStateException). Terminal operations fall into categories: reduction (reduce, collect), matching (anyMatch, allMatch, noneMatch), finding (findFirst, findAny), counting (count), iteration with side effects (forEach, forEachOrdered), and array/collection materialization (toArray, toList). Some terminal operations are short-circuiting (anyMatch, findFirst) and can terminate before examining the entire stream; others (forEach, count in most cases, collect) must examine every element. This entry covers every terminal operation in depth, the reduce() method and its three overloads with the combiner function's role in parallel execution, the Collectors framework as the primary mechanism for complex terminal reduction, short-circuiting semantics and their interaction with infinite streams, the single-use constraint and its rationale, and the choice between equivalent terminal operations for performance and clarity.
Optional
Optional<T>, introduced in Java 8, is a container object that may or may not hold a non-null value, designed to make the possible absence of a value explicit in a method's type signature rather than implicit through a nullable return type. Optional.of(value) creates a present Optional and throws NullPointerException if value is null; Optional.empty() creates an absent Optional; Optional.ofNullable(value) creates either a present or empty Optional depending on whether value is null. The design intent, stated explicitly in the Javadoc, is as a return type for methods where the absence of a result is a valid and expected outcome, communicating that absence through the type system instead of through null, with the goal of reducing NullPointerException at the API boundary. Optional is explicitly not intended for use as a field type, a method parameter type, or inside collections, and the JDK team has stated that misuse in these contexts works against its design intent. This entry covers the complete Optional API including creation, value extraction, conditional execution, transformation and chaining, the primitive specializations OptionalInt/OptionalLong/OptionalDouble, and the established conventions for where Optional should and should not be used.
Default Methods
Default methods, introduced in Java 8, are methods declared in an interface with a body, marked with the default keyword, providing a concrete implementation that implementing classes inherit automatically without being forced to override it. Before Java 8, interfaces could declare only abstract methods, meaning adding any new method to an interface would break every existing implementation — a problem that became critical when the Streams API needed to add methods like forEach(), stream(), and removeIf() to the existing Collection-family interfaces without breaking the entire ecosystem of third-party collection implementations. Default methods solved this by allowing interface evolution: new methods can be added to an interface with a default implementation, and existing implementing classes continue to compile and work without modification, inheriting the default behavior until they choose to override it. This entry covers the design motivation and interface evolution problem default methods solve, the complete syntax and inheritance rules, the diamond problem and Java's resolution rules for conflicting default methods from multiple interfaces, the explicit disambiguation syntax using InterfaceName.super.method(), the interaction between default methods and abstract classes, and the static method companion feature introduced alongside default methods.