gatherers/: windowFixed, windowSliding, fold, scan, mapConcurrent, a custom Gatherer (dedupeConsecutive, takeUntil), and a Collector-vs-Gatherer comparison. Verified: windowSliding emits one short window on a too-short stream rather than shrinking to empty like windowFixed; mapConcurrent waits for in-flight siblings to finish (~901ms) rather than cancelling them when one mapper throws. scoped-values/: ScopedValue API and rebinding, why plain threads (virtual or platform) do not inherit a binding while a StructuredTaskScope subtask does, and the memory cost vs InheritableThreadLocal measured two ways -- a noisy heap-delta first pass, then a precise jcmd GC.class_histogram object census (5 shared Carrier objects vs ~700,000 copied ThreadLocalMap/Entry objects for 100,000 threads). Companion code for the ankurm.com posts "Java Stream Gatherers (JEP 485)" and "Scoped Values vs ThreadLocal in Java 25: Migration Guide with Virtual Threads". Co-Authored-By: Claude Sonnet 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01FtpJvZfg4nvLvtzgJTDWpB
86 lines
4.2 KiB
Java
86 lines
4.2 KiB
Java
import java.util.NoSuchElementException;
|
|
|
|
/**
|
|
* The core API (JEP 506, final in Java 25, no preview flag): newInstance(), where(...).run(...) /
|
|
* .call(...), get(), isBound(), orElse(), orElseThrow(), and rebinding -- a nested where() that
|
|
* shadows an outer binding for the extent of its own block, then hands control back to the outer
|
|
* value once that block exits.
|
|
* Chapter: docs/17-scoped-values-migration.md
|
|
*/
|
|
public class ScopedValuesBasics {
|
|
|
|
static final ScopedValue<String> USER = ScopedValue.newInstance();
|
|
|
|
public static void main(String[] args) {
|
|
basicBindAndRead();
|
|
unboundThrowsByDefault();
|
|
orElseAndOrElseThrow();
|
|
rebindingShadowsThenRestores();
|
|
callReturnsAValue();
|
|
}
|
|
|
|
static void basicBindAndRead() {
|
|
ScopedValue.where(USER, "ankur").run(() -> {
|
|
Check.that(USER.isBound(), "isBound() is true inside where().run()");
|
|
Check.that(USER.get().equals("ankur"), "get() returns \"ankur\" inside the binding");
|
|
deepMethodSeesTheSameBinding();
|
|
});
|
|
Check.that(!USER.isBound(), "isBound() is false again once run() has returned");
|
|
}
|
|
|
|
static void deepMethodSeesTheSameBinding() {
|
|
// No parameter was passed here -- USER is visible because this call happens ON THE SAME
|
|
// THREAD while the binding is active, not because of anything passed explicitly.
|
|
Check.that(USER.get().equals("ankur"), "a method several frames deep, given no parameter, still sees the binding");
|
|
}
|
|
|
|
static void unboundThrowsByDefault() {
|
|
try {
|
|
USER.get();
|
|
Check.that(false, "expected get() to throw outside any binding");
|
|
} catch (NoSuchElementException e) {
|
|
Check.that(true, "get() outside a binding throws NoSuchElementException, not null or a default value");
|
|
}
|
|
}
|
|
|
|
static void orElseAndOrElseThrow() {
|
|
Check.that(USER.orElse("anonymous").equals("anonymous"), "orElse(fallback) returns the fallback when unbound, no exception");
|
|
ScopedValue.where(USER, "ankur").run(() -> Check.that(USER.orElse("anonymous").equals("ankur"), "orElse(fallback) returns the real value when bound"));
|
|
|
|
try {
|
|
USER.orElseThrow(() -> new IllegalStateException("no user in scope"));
|
|
Check.that(false, "expected orElseThrow to throw when unbound");
|
|
} catch (IllegalStateException e) {
|
|
Check.that(e.getMessage().equals("no user in scope"), "orElseThrow(supplier) throws exactly the supplied exception when unbound");
|
|
}
|
|
}
|
|
|
|
static void rebindingShadowsThenRestores() {
|
|
ScopedValue.where(USER, "ankur").run(() -> {
|
|
Check.that(USER.get().equals("ankur"), "outer binding: ankur");
|
|
ScopedValue.where(USER, "support-bot").run(() -> {
|
|
// A nested where() on the SAME ScopedValue shadows the outer binding for the
|
|
// extent of this inner block -- this is rebinding, and it's the mechanism a
|
|
// request handler uses to say "everything I call from here on behaves as
|
|
// support-bot" without touching the outer caller's binding at all.
|
|
Check.that(USER.get().equals("support-bot"), "inner binding shadows the outer one: support-bot");
|
|
methodInsideInnerScope();
|
|
});
|
|
// Back outside the inner where(): the outer binding is exactly as it was, not
|
|
// mutated, not cleared -- rebinding is scoped, not an assignment.
|
|
Check.that(USER.get().equals("ankur"), "outer binding is restored, unchanged, once the inner where() block exits: ankur");
|
|
});
|
|
}
|
|
|
|
static void methodInsideInnerScope() {
|
|
Check.that(USER.get().equals("support-bot"), "a method called from inside the inner scope sees the REBOUND value, not the original");
|
|
}
|
|
|
|
static void callReturnsAValue() {
|
|
// .call(...) is where().run()'s twin for when you need a result back, propagating a
|
|
// checked exception type X through Carrier.call's signature.
|
|
int length = ScopedValue.where(USER, "ankur").call(() -> USER.get().length());
|
|
Check.that(length == 5, "call(...) returns whatever the lambda returns -- USER.get().length() == 5");
|
|
}
|
|
}
|