Scoped Values practice questions

From OCP Java SE 25 (1Z0-831) · 17 questions on this topic

Scoped Values practice questions from OCP Java SE 25 (1Z0-831). This pack has 17 questions tagged Scoped Values, drawn from its timed mock exams. 8 of them are worked through in full below — the question, every option, why each is right or wrong, and the explanation.

Worked examples for Scoped Values

  1. Question 1

    What is the result of running this program? ```java public class Main { static final ScopedValue<String> V = ScopedValue.newInstance(); public static void main(String[] args) { System.out.print(V.get()); } } ```

    1. A. Prints an empty line

      No empty line is printed; the exception is thrown by get() before System.out.print ever runs.

    2. B. Prints null

      get() never returns null to signal absence; it throws instead. Use orElse(...) if you want a fallback value.

    3. C. Throws NoSuchElementExceptionCorrect answer

      newInstance() produces a value with no binding and nothing wraps the read in a where(...).run(...) scope, so calling get() with no active binding throws NoSuchElementException ("ScopedValue not bound") before anything is printed.

    4. D. Compilation fails: V must be initialized with a value

      newInstance() legitimately creates an unbound ScopedValue; declaring one without a value compiles fine, so the failure is at runtime, not compile time.

    Explanation

    ScopedValue.newInstance() produces a value with no binding, and nothing here wraps the read in a where(...).run(...) scope. Calling get() with no active binding throws NoSuchElementException ("ScopedValue not bound") before any output is produced; it does not return null. If code might run outside a binding, guard with isBound() or read via orElse(default) instead.

  2. Question 2

    Which two statements about `ScopedValue` in JDK 25 are correct? (Choose two.)

    1. A. A binding established with where(...).run(...) is not visible to a child created with new Thread(...).Correct answer

      A ScopedValue binding is confined to the dynamic extent of its run/call on the establishing thread and is not inherited by a plain new Thread child, where isBound() is false.

    2. B. Calling get() on an unbound ScopedValue returns null.

      An unbound get() throws NoSuchElementException; it never returns null. Use orElse(default) or check isBound() for a safe read.

    3. C. ScopedValue provides no set method; you rebind only by nesting a new where(...).run(...).Correct answer

      ScopedValue is immutable: the public API is get, isBound, orElse, and orElseThrow with no set, so the only way to change the observed value is to nest another where(...).run(...), which shadows the outer binding for its extent and restores it afterward.

    4. D. ScopedValue.runWhere(value, x, task) is a static shortcut for binding and running in JDK 25.

      There is no static ScopedValue.runWhere/callWhere in JDK 25; those names existed only in earlier previews. Binding goes through ScopedValue.where(value, x) to obtain a Carrier, then .run(...) or .call(...).

    Explanation

    Two of the true traps for scoped values sit in this question. A binding is confined to the dynamic extent of its run/call on the establishing thread and is not inherited by a plain new Thread child, and the type is immutable, so the public API is get/isBound/orElse/orElseThrow with no set and rebinding happens only by nesting another where(...).run(...). The remaining traps to reject are that an unbound get() throws NoSuchElementException rather than returning null, and that binding always goes through where(value, x) to obtain a Carrier followed by run/call, with no static runWhere convenience in JDK 25.

  3. Question 3

    Two tasks run in order on the same pooled platform thread (each `get()` blocks until that task finishes). What does this program print? ```java import java.util.concurrent.*; public class Main { static final ThreadLocal<String> TL = new ThreadLocal<>(); static final ScopedValue<String> SV = ScopedValue.newInstance(); public static void main(String[] args) throws Exception { ExecutorService pool = Executors.newSingleThreadExecutor(); StringBuilder sb = new StringBuilder(); ScopedValue.where(SV, "sv").run(() -> { TL.set("main"); try { pool.submit(() -> { TL.set("task1"); sb.append(SV.orElse("none")).append("/").append(TL.get()); }).get(); pool.submit(() -> sb.append("|").append(SV.orElse("none")).append("/").append(TL.get())).get(); } catch (InterruptedException | ExecutionException e) { throw new RuntimeException(e); } }); pool.shutdown(); sb.append("|").append(TL.get()); System.out.println(sb); } } ```

    1. A. none/task1|none/task1|mainCorrect answer

      The worker is a different thread with no scoped-value binding of its own, so both tasks read SV.orElse("none") as none; task 1 sets the worker's ThreadLocal to task1 and nothing clears it, so task 2 on the same recycled worker still sees task1, while main's own ThreadLocal is untouched and reads main.

    2. B. none/main|none/main|main

      Assumes a plain ThreadLocal is copied to the executing thread at submit time; it is not — not even InheritableThreadLocal does that (it copies at thread creation, and this worker existed before the value was set).

    3. C. none/task1|none/null|main

      Assumes each task starts with clean thread-locals, i.e. that the pool wipes worker state between tasks; nothing does, so task 2 still sees task1 — only TL.remove() in a finally block would clear it.

    4. D. sv/task1|sv/task1|main

      Assumes a scoped value binding is inherited by whatever thread runs a task submitted from inside the scope; bindings are inherited only by threads forked in a structured task scope, never by an arbitrary pooled thread, so the worker reads none.

    Explanation

    Trace: the pool's worker is a different thread from main, and neither mechanism crosses the submit boundary the way a novice expects. The scoped value binding belongs to main's execution of `run(...)`; the worker thread has no binding of its own, so both tasks read `SV.orElse("none")` as `none`. The ThreadLocal is per-thread state on the *worker*: task 1 sets it to `task1`, task 1 ends, and nothing removes it — so task 2, running on that same recycled worker, still sees `task1`. That is the classic pooled-thread ThreadLocal leak. Meanwhile main's own ThreadLocal entry is untouched by anything the worker did, so the final read prints `main`. Output: `none/task1|none/task1|main`. Why the others are wrong: `sv/task1|sv/task1|main` assumes a scoped value binding is inherited by whatever thread runs a task submitted from inside the scope. Bindings are inherited only by threads forked in a structured task scope, never by an arbitrary pooled thread. `none/main|none/main|main` assumes a plain ThreadLocal is copied to the executing thread at submit time — that is not even what InheritableThreadLocal does (it copies at thread *creation*, and this worker existed before the value was set). `none/task1|none/null|main` assumes each task starts with clean thread-locals, i.e. that the pool wipes worker state between tasks. Nothing does; only `TL.remove()` in a finally block would. Exam tip: on a pooled thread a ThreadLocal outlives the task that set it — the leak scoped values were designed to make impossible, because a ScopedValue binding cannot outlive its `run`/`call`. And note the flip side of the same coin: a scoped value that is not visible in the pool is not a bug, it is the guarantee.

  4. Question 4

    A `ScopedValue` and an `InheritableThreadLocal` are both set on the main thread, then read from a plain child thread that is started and joined inside the binding. What does this print? ```java public class Main { static final ScopedValue<String> SV = ScopedValue.newInstance(); static final InheritableThreadLocal<String> ITL = new InheritableThreadLocal<>(); public static void main(String[] args) throws InterruptedException { ITL.set("tl"); ScopedValue.where(SV, "sv").run(() -> { Thread t = new Thread(() -> System.out.print(SV.isBound() + " " + ITL.get())); t.start(); try { t.join(); } catch (InterruptedException e) { } }); } } ```

    1. A. true sv

      Assumes the ScopedValue binding was inherited by the plain child; it is not, so isBound() is false and SV.get() would actually throw in the child.

    2. B. true tl

      Gets the thread-local right but wrongly claims the ScopedValue is bound in the child; a plain new Thread child does not inherit the binding, so isBound() is false.

    3. C. false null

      Gets the ScopedValue right but wrongly claims the InheritableThreadLocal was not propagated; its parent value is copied to the child at thread-creation time, so it reads "tl".

    4. D. false tlCorrect answer

      A ScopedValue binding is confined to the run on the establishing thread and is not inherited by a plain new Thread child, so isBound() is false; an InheritableThreadLocal copies the parent's value to the child, so it reads "tl". The child is started and joined inside the binding, making the output deterministic.

    Explanation

    The sharp contrast to memorize is that a plain new Thread child inherits an InheritableThreadLocal but NOT a ScopedValue binding. A scoped-value binding is confined to the dynamic extent of the run on the thread that established it, whereas an InheritableThreadLocal copies the parent's value into a child at thread-creation time. Scoped values only reach child threads through structured constructs, never an ad-hoc new Thread(...), so in the child the scoped value is unbound while the inheritable thread-local still holds its value.

  5. Question 5

    What does this print? ```java public class Main { static final ScopedValue<String> V = ScopedValue.newInstance(); public static void main(String[] args) throws Exception { String r = ScopedValue.where(V, "x").call(() -> V.get() + "!"); System.out.print(r); } } ```

    1. A. x!Correct answer

      The Carrier's call(...) runs a value-returning operation while the binding is active and returns its result; V is bound to "x", so the lambda returns "x!", which is assigned to r and printed.

    2. B. !

      "!" alone would mean V.get() returned an empty string, but it returns the bound value "x".

    3. C. null!

      Assumes an unbound read yields null, but V is bound and get() never returns null anyway.

    4. D. Compilation fails: call() returns void

      call(...) returns the operation's result (here a String); it is run(...) that returns void, so the assignment compiles.

    Explanation

    Unlike run(Runnable), which returns void, the Carrier's call(...) executes a value-returning operation while the binding is active and returns its result to the caller. Here the scoped value is bound while the lambda runs, so its get() yields the bound value and the lambda's returned string is assigned and printed. call(...) may also propagate a checked exception, which is why the enclosing method declares throws Exception; reach for call when you need a result out of the bound scope.

  6. Question 6

    Which two statements about `ScopedValue` and `ThreadLocal` in Java 25 are correct? (Choose two.)

    1. A. ScopedValue.get() throws NoSuchElementException when no binding is in effect, whereas ThreadLocal.get() returns null on a thread that never called set().Correct answer

      Correct: an unbound ScopedValue read is an error — get() with no binding throws NoSuchElementException — whereas a ThreadLocal never set on this thread simply returns null (or its withInitial value); soft behaviour must be requested with isBound()/orElse().

    2. B. A ScopedValue binding is discarded automatically when the run(...) or call(...) that established it returns, so ScopedValue has no equivalent of ThreadLocal.remove().Correct answer

      Correct: a ScopedValue binding's lifetime is exactly the dynamic extent of run(...)/call(...); when that returns (normally or by throwing) the runtime tears the binding down, so there is nothing to clean up and hence no remove().

    3. C. Because a ScopedValue lives in a static final field, the value one thread binds with where(...).run(...) is visible to every other thread until it is rebound.

      Confuses the key with the binding; the static final field is the shared key, but the binding is per-thread and per-scope, so two threads binding the same ScopedValue each see only their own value and a thread with no binding sees none.

    4. D. ScopedValue.newInstance(Supplier) supplies a default that get() returns when no binding is in effect, mirroring ThreadLocal.withInitial(Supplier).

      Invents an API; ScopedValue.newInstance() takes no arguments and passing a supplier is a compile error — there is no per-ScopedValue default, only orElse/orElseThrow at the read site.

    Explanation

    Why `A ScopedValue binding is discarded automatically when the run(...) ...` is correct: the binding's lifetime is exactly the dynamic extent of the operation. When `run`/`call` returns — normally or by throwing — the binding is torn down by the runtime. That is why the API has no `remove()`: there is nothing left to clean up, and therefore no way to leak a value onto a long-lived (or pooled) thread. Why `ScopedValue.get() throws NoSuchElementException when no binding ...` is correct: an unbound read is an error, not a silent null. `V.get()` with no binding throws `java.util.NoSuchElementException: ScopedValue not bound`. A plain `ThreadLocal` that was never `set` on this thread simply returns `null` (or the `withInitial` supplier's value). If you want the soft behaviour from a ScopedValue you must ask for it explicitly with `isBound()`, `orElse(...)` or `orElseThrow(...)`. Why the others are wrong: `ScopedValue.newInstance(Supplier) supplies a default that get() ...` invents an API. `ScopedValue.newInstance()` takes no arguments — there is no per-ScopedValue default, and passing a supplier is a compile error (`required: no arguments`). The only defaulting mechanism is `orElse`/`orElseThrow` at the read site. `Because a ScopedValue lives in a static final field, the value one thread binds ...` confuses the *key* with the *binding*. The static final field is the key, shared by everyone; the binding is per-thread and per-scope. Two threads can run `where(V, "one")` and `where(V, "two")` at the same time and each sees only its own value, and a thread with no binding sees none at all. Exam tip: ScopedValue trades ThreadLocal's three weaknesses — unconstrained mutability, unbounded lifetime, and expensive inheritance — for one-way, scope-bounded, immutable bindings. Expect the exam to test the consequences: no set, no remove, no default supplier, and an exception rather than null on an unbound read.

  7. Question 7

    What does this print? ```java public class Main { static final ScopedValue<String> USER = ScopedValue.newInstance(); public static void main(String[] args) { System.out.print(USER.isBound() + " " + USER.orElse("dflt")); } } ```

    1. A. false dfltCorrect answer

      With no where(...).run(...) scope, USER is unbound, so isBound() returns false without throwing and orElse("dflt") returns the supplied fallback, giving "false dflt".

    2. B. true dflt

      true would require an active binding, but nothing bound USER here, so isBound() is false.

    3. C. false null

      orElse returns its argument ("dflt") when unbound, not null; a ScopedValue never yields null for an absent binding.

    4. D. Throws NoSuchElementException

      Only get() throws when unbound; isBound() and orElse(...) are the safe, non-throwing readers, so nothing is thrown here.

    Explanation

    With no where(...).run(...) scope around this code, the scoped value is unbound on the main thread. isBound() reports the presence of a binding without throwing, so it returns false, and orElse(default) returns its supplied fallback when there is no binding. isBound() and orElse(default) are the two ways to read a scoped value that might be unbound without risking NoSuchElementException, so reach for them instead of guarding get().

  8. Question 8

    The `Carrier` returned by `where` is held in a local variable before it is used. What does this program print? ```java public class Main { static final ScopedValue<String> V = ScopedValue.newInstance(); public static void main(String[] args) { ScopedValue.Carrier c = ScopedValue.where(V, "bound"); System.out.print(V.isBound()); c.run(() -> System.out.print("-" + V.get())); System.out.println("-" + V.isBound()); } } ```

    1. A. true-bound-false

      Gets the teardown right but assumes the binding starts at the `where` call rather than at `run`; where only builds a Carrier, so isBound() before c.run(...) is false, not true.

    2. B. false-bound-falseCorrect answer

      where(V, "bound") only builds an immutable Carrier and binds nothing, so the first isBound() is false; the binding exists only while c.run(...) executes, where V.get() prints bound; after run returns the binding is gone, so the last isBound() is false.

    3. C. false-bound-true

      Gets the start right but assumes the binding survives the operation — the ThreadLocal habit scoped values exist to break; the binding is torn down when run() returns, so the final isBound() is false.

    4. D. true-bound-true

      Assumes where installs the binding immediately and leaves it installed, i.e. that a Carrier behaves like ThreadLocal.set; it does neither, so both the first and last isBound() are false.

    Explanation

    Trace: `ScopedValue.where(V, "bound")` does not bind anything. It builds an immutable `ScopedValue.Carrier` — a recipe of key/value pairs — and hands it back. The binding exists only while the Carrier's `run` (or `call`) is executing. So the first `isBound()`, before `c.run(...)`, prints `false`; inside the Runnable `V.get()` prints `bound`; after `run` returns the binding is gone and the last `isBound()` prints `false`. Output: `false-bound-false`. Why the others are wrong: `true-bound-true` assumes `where` installs the binding immediately and leaves it installed, i.e. that a Carrier behaves like `ThreadLocal.set`. `true-bound-false` gets the teardown right but assumes the binding starts at the `where` call rather than at `run`. `false-bound-true` gets the start right but assumes the binding survives the operation, which is the ThreadLocal habit that scoped values exist to break. Exam tip: `where` = build a Carrier; `run`/`call` = open the scope, execute, close the scope. A Carrier is immutable and reusable — you can hold one in a field and `run` it many times — but it carries no binding until it is run. The reverse trap is chaining `where(A, 1).where(B, 2).run(...)`, where the second `where` returns a *new* Carrier holding both pairs.

Practise all 17 Scoped Values questions

OCP Java SE 25 has the full set, inside timed mock exams that mirror real exam conditions — every question with a worked explanation.

Open OCP Java SE 25

Other topics in this pack