How does ServiceLoader instantiate providers lazily, and how does the stream()/Provider API let you inspect a provider before creating it?
answer
- load() discovers; iteration/get() instantiates
- Instances cached; reload() to re-scan
- stream() -> Stream<Provider<T>>
- Provider.type() inspects class without instantiating; get() builds it
- provider() static method or no-arg constructor; errors at iteration time
basics
~20 sServiceLoader only creates a provider object when you actually reach it while iterating, not up front. Since Java 9, ServiceLoader.stream() gives a stream of Provider handles, each exposing type() so you can check the class and call get() to instantiate only the one(s) you want.
solid answer
~40 sServiceLoader is lazy: ServiceLoader.load() does no instantiation; it just prepares discovery. Each provider's instance is created the first time you reach it during iteration, and successful instances are cached so re-iterating returns the same objects (call reload() to discard the cache and re-scan). Classic iteration forces you to instantiate a provider to learn anything about it. Java 9 added ServiceLoader.stream(), which returns a Stream<ServiceLoader.Provider<T>>. A Provider is a lightweight handle: provider.type() returns the Class<? extends T> without instantiating, and provider.get() instantiates on demand. This lets you filter by annotations or class before paying construction cost — e.g. pick the provider whose type carries a particular annotation, or the highest-priority one, and only call get() on the winner. This is the idiomatic way to do selective or prioritized provider loading.
code
java · 21 lines// Select the highest-priority provider WITHOUT instantiating the rest.
// Priority is read from an annotation on the provider class via type().
@Retention(RetentionPolicy.RUNTIME)
@interface Priority { int value(); }
MyService best = ServiceLoader.load(MyService.class).stream()
.max(Comparator.comparingInt(p -> {
Priority a = p.type().getAnnotation(Priority.class); // no instantiation
return a != null ? a.value() : 0;
}))
.map(ServiceLoader.Provider::get) // construct only the winner
.orElseThrow(() -> new IllegalStateException("no MyService provider"));
// Skip a broken provider instead of aborting the whole iteration:
List<MyService> usable = ServiceLoader.load(MyService.class).stream()
.flatMap(p -> {
try { return Stream.of(p.get()); }
catch (ServiceConfigurationError e) { return Stream.empty(); }
})
.toList();go deeper
Knows providers are created when you loop over the ServiceLoader, not before.
Explains lazy instantiation, caching, reload(), and the no-arg-constructor / provider() requirement.
Uses stream()/Provider.type() for inspection-before-instantiation, handles ServiceConfigurationError per provider, and knows ordering is unspecified.
Designs provider-selection strategies (priority annotations, factory methods), reasons about cost of construction, thread-safety, and lifecycle/reload semantics in long-running apps.
## What 'lazy' means here **Lazy instantiation** means an object is not constructed until it is actually needed. `ServiceLoader` is built around this: 1. `ServiceLoader.load(MyService.class)` performs **discovery setup** — it figures out *which* providers exist (by reading module descriptors or `META-INF/services`) but **constructs none of them**. 2. As you **iterate** (`for (MyService s : loader)`), the loader constructs **the next provider only when the iterator advances to it**. If you stop early, the remaining providers are never built. 3. Each constructed instance is **cached** inside the ServiceLoader. Iterating the same loader again yields the **same** instances (not fresh ones). To throw away the cache and re-discover (e.g. after dropping in a new jar), call `loader.reload()`. Why lazy + cached? Constructing a provider can be expensive (open files, JNI, allocate buffers). If a consumer only needs the first matching provider, building the rest is wasted work — laziness avoids it. Caching means repeated lookups are cheap and stable. ## The construction contract When the loader does construct a provider it uses, in order of preference: - a **public static `provider()` method** on the provider class (the *provider factory* — it returns the service instance, may even return a different class), or - a **public no-argument constructor**. If neither exists, or construction throws, you get a **`ServiceConfigurationError`** — and critically it is thrown **during iteration / `get()`**, not at `load()` time, precisely because discovery and instantiation are separated. ## The limitation classic iteration has With the plain `Iterable` API, the only way to find out anything about a provider — its class, an annotation on it, a priority field — is to **instantiate it**. If you have ten providers and want the one annotated `@Primary`, you would construct all ten just to inspect them. That is exactly the cost laziness was trying to save. ## The Java 9 fix: `stream()` + `Provider` Java 9 added `ServiceLoader.stream()`, returning `Stream<ServiceLoader.Provider<T>>`. A `ServiceLoader.Provider<T>` is a **deferred handle** to a single provider with two methods: - **`type()`** → `Class<? extends T>`: the provider's class, available **without instantiating it**. You can read annotations, check the simple name, etc. - **`get()`** → `T`: instantiate the provider **on demand** (subject to the same caching). So you can inspect *cheaply* via `type()` and pay for construction *only* on the chosen provider(s): ```java MyService chosen = ServiceLoader.load(MyService.class).stream() .filter(p -> p.type().isAnnotationPresent(Primary.class)) .map(ServiceLoader.Provider::get) // instantiate only the survivors .findFirst() .orElseThrow(); ``` This is the **idiomatic pattern for selective / prioritized loading**: sort or filter on `type()`, then `get()` the winner. You honour the laziness contract — annotation-driven selection without constructing the rejected providers. ## Other behavioural notes - **Order is unspecified.** Both iteration and stream order roughly follow discovery order; never depend on it for correctness — encode priority in an annotation or interface method and sort explicitly. - **Not thread-safe.** A single ServiceLoader instance should not be iterated concurrently from multiple threads. - **One bad provider can poison iteration.** A `ServiceConfigurationError` from one provider can interrupt a `for` loop; the stream lets you `try/catch` around each `get()` to skip a broken provider gracefully.
- Why prefer stream()/Provider over the plain Iterable when selecting one of several providers?Because Provider.type() lets you inspect each provider's class (annotations, name, priority) without constructing it, then call get() only on the chosen one. Plain iteration would force you to instantiate every candidate just to inspect it.
- You added a new provider jar at runtime but ServiceLoader still returns the old set. Why?ServiceLoader caches discovered/instantiated providers. Call reload() to clear the cache and re-run discovery (assuming the new jar is actually on the active class/module path of that loader).
saying these in an interview costs you the question
- Saying load() instantiates all providers eagerly
- Believing each iteration creates fresh instances (they're cached until reload())
- Thinking you must instantiate a provider to read its annotations (use Provider.type())
- Relying on a fixed provider ordering for correctness
- Expecting ServiceConfigurationError at load() rather than at iteration/get()