From 836dda68ac17e555c66dfdc37db7fd892130ead4 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 23 Oct 2026 06:11:55 +0000 Subject: [PATCH] Add Java 25 Scoped Values vs ThreadLocal examples --- .../test/java/pl/mperor/lab/java/Java25.java | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/JavaReleases/src/test/java/pl/mperor/lab/java/Java25.java b/JavaReleases/src/test/java/pl/mperor/lab/java/Java25.java index 150dc8d..143dde9 100644 --- a/JavaReleases/src/test/java/pl/mperor/lab/java/Java25.java +++ b/JavaReleases/src/test/java/pl/mperor/lab/java/Java25.java @@ -4,6 +4,8 @@ import org.junit.jupiter.api.Test; import pl.mperor.lab.common.TestUtils; +import java.util.NoSuchElementException; + /// Java 25 (September 2025) /// [JDK 25](https://openjdk.org/projects/jdk/25) /// @@ -98,4 +100,52 @@ void describe() { } } + static final ScopedValue USER = ScopedValue.newInstance(); + + @Test + public void testScopedValueIsBoundOnlyInsideRunScope() { + Assertions.assertFalse(USER.isBound()); + + ScopedValue.where(USER, "John Doe").run(() -> { + Assertions.assertTrue(USER.isBound()); + Assertions.assertEquals("John Doe", USER.get()); + }); + + // the binding is torn down as soon as run() returns + Assertions.assertFalse(USER.isBound()); + Assertions.assertThrows(NoSuchElementException.class, USER::get); + } + + @Test + public void testNestedRebindingRestoresOuterValueAfterward() { + ScopedValue.where(USER, "John Doe").run(() -> { + Assertions.assertEquals("John Doe", USER.get()); + + ScopedValue.where(USER, "Jane Doe").run(() -> Assertions.assertEquals("Jane Doe", USER.get())); + + // the nested run() only rebinds USER for its own body; the outer binding is unaffected + Assertions.assertEquals("John Doe", USER.get()); + }); + } + + @Test + public void testCallReturnsValueComputedUnderTheBinding() throws Exception { + String greeting = ScopedValue.where(USER, "John Doe").call(() -> "Hello, " + USER.get()); + Assertions.assertEquals("Hello, John Doe", greeting); + } + + @Test + public void testScopedValueHasNoSetUnlikeThreadLocal() { + var legacy = new ThreadLocal(); + legacy.set("John Doe"); + legacy.set("Jane Doe"); // ThreadLocal is mutable: set() silently replaces the value, anywhere, anytime + Assertions.assertEquals("Jane Doe", legacy.get()); + legacy.remove(); // must be cleared by hand, or it leaks for the thread's lifetime (e.g. in a pool) + + // ScopedValue has no set()/remove(): the only way to associate a value is where(...).run()/.call(...), + // and the binding is automatically torn down when that call returns - no leak is possible. + ScopedValue.where(USER, "John Doe").run(() -> Assertions.assertEquals("John Doe", USER.get())); + Assertions.assertFalse(USER.isBound()); + } + }