From 814c32b5cea0fd9d625c424501ea32762dde06e1 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 24 Sep 2026 12:27:27 +0000 Subject: [PATCH] Add lazy-constants module: LazyConstant (JEP 531) against the holder idiom and double-checked locking Basics, failure semantics on 26 vs 27, lazy collections, API across 25/26/27, preview-flag traps and a JMH read-path benchmark (jars fetched and sha1-checked, not committed). Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01TF9JWFvJSNm6HVzswzZU5a --- README.md | 1 + lazy-constants/README.md | 17 +++ lazy-constants/broken/NoPreviewFlag.java | 4 + lazy-constants/broken/ReassignInSupplier.java | 7 ++ lazy-constants/broken/RemovedApi.java | 7 ++ lazy-constants/broken/StillStable.java | 4 + lazy-constants/jmh/.gitignore | 1 + lazy-constants/jmh/fetch.sh | 15 +++ lazy-constants/jmh/run-jmh.sh | 15 +++ lazy-constants/jmh/src/bench/AccessBench.java | 52 ++++++++ lazy-constants/output/01-old-ways.txt | 29 +++++ lazy-constants/output/02-basics.txt | 20 +++ lazy-constants/output/03-failures.txt | 13 ++ lazy-constants/output/04-collections.txt | 26 ++++ .../output/05-api-across-releases.txt | 118 ++++++++++++++++++ lazy-constants/output/06-preview-flags.txt | 23 ++++ lazy-constants/output/07-jmh.txt | 9 ++ lazy-constants/run.sh | 62 +++++++++ lazy-constants/src/Basics.java | 35 ++++++ lazy-constants/src/Failures.java | 25 ++++ lazy-constants/src/LazyCollections.java | 25 ++++ lazy-constants/src/OldWays.java | 35 ++++++ lazy-constants/src/Service.java | 29 +++++ lazy-constants/src/StableValue25.java | 20 +++ lazy-constants/src/ToStringStates.java | 14 +++ 25 files changed, 606 insertions(+) create mode 100644 lazy-constants/README.md create mode 100644 lazy-constants/broken/NoPreviewFlag.java create mode 100644 lazy-constants/broken/ReassignInSupplier.java create mode 100644 lazy-constants/broken/RemovedApi.java create mode 100644 lazy-constants/broken/StillStable.java create mode 100644 lazy-constants/jmh/.gitignore create mode 100755 lazy-constants/jmh/fetch.sh create mode 100755 lazy-constants/jmh/run-jmh.sh create mode 100644 lazy-constants/jmh/src/bench/AccessBench.java create mode 100644 lazy-constants/output/01-old-ways.txt create mode 100644 lazy-constants/output/02-basics.txt create mode 100644 lazy-constants/output/03-failures.txt create mode 100644 lazy-constants/output/04-collections.txt create mode 100644 lazy-constants/output/05-api-across-releases.txt create mode 100644 lazy-constants/output/06-preview-flags.txt create mode 100644 lazy-constants/output/07-jmh.txt create mode 100755 lazy-constants/run.sh create mode 100644 lazy-constants/src/Basics.java create mode 100644 lazy-constants/src/Failures.java create mode 100644 lazy-constants/src/LazyCollections.java create mode 100644 lazy-constants/src/OldWays.java create mode 100644 lazy-constants/src/Service.java create mode 100644 lazy-constants/src/StableValue25.java create mode 100644 lazy-constants/src/ToStringStates.java diff --git a/README.md b/README.md index b10bba0..bbe2a30 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,7 @@ These modules keep only sources, a `run.sh` and their transcripts (`output/`). T | [`jep511/`](jep511) | Module Import Declarations in Java 25 (JEP 511) | `import module java.base;` against seven single imports, ambiguity errors and how single-type, on-demand and same-package names beat a module import, what one module import does and does not bring in (`java.sql` does not give `java.base`; `java.se` needs `--add-modules`), named modules, JEP 512 compact files, jshell's default imports, identical bytecode | | [`patterns/`](patterns) | Pattern Matching in Java: switch, Record Patterns and Primitive Patterns (JDK 21 to 27) | type and record patterns, guards and dominance errors, sealed exhaustiveness (with JDK 27's new `missing patterns` lines), `MatchException` from a throwing accessor and from separate compilation, unnamed patterns, primitive patterns with `--enable-preview` and the preview class-file trap | | [`sealed/`](sealed) | Sealed Classes and Interfaces: Modelling Domains with Exhaustive switch | a `Payment` model, `permits` and the three subclass modifiers, the `default` trap, generic and recursive hierarchies, the visitor for comparison, run-time enforcement and modules | +| [`lazy-constants/`](lazy-constants) | Lazy Constants in JDK 27 (JEP 531): Replacing Double-Checked Locking | `LazyConstant` against the holder idiom and double-checked locking, the 26 to 27 failure-semantics change, `List`/`Map`/`Set.ofLazy`, the preview-flag traps and a JMH read-path benchmark | ## Captured output (`docs/output/`) diff --git a/lazy-constants/README.md b/lazy-constants/README.md new file mode 100644 index 0000000..1976c68 --- /dev/null +++ b/lazy-constants/README.md @@ -0,0 +1,17 @@ +# lazy-constants — Lazy Constants (JEP 531, third preview in JDK 27) + +Companion code for the ankurm.com article **Lazy Constants in JDK 27 (JEP 531): Replacing Double-Checked Locking**. The explanation lives in the article; +this folder holds the runnable sources and the transcripts they produced. + +```bash +JDK25=/path/to/jdk-25 JDK26=/path/to/jdk-26 JDK27=/path/to/jdk-27 [JDK21=/path/to/jdk-21] ./run.sh # regenerates output/*.txt +RUN_JMH=0 ./run.sh # skip the 3-minute benchmark +``` + +- `src/` — `OldWays` (holder idiom and double-checked locking, plain Java), `Basics`, `ToStringStates`, `Service`, `Failures`, `LazyCollections` (JDK 27, `--enable-preview`), `StableValue25` (the JDK 25 API). +- `broken/` — code that fails to compile on purpose: the old `StableValue` name, the two methods removed in 27, no `--enable-preview`, a `set` that does not exist. +- `jmh/` — a JMH 1.37 benchmark of the read path. `./jmh/fetch.sh` downloads the jars from Maven Central and checks their sha1; they are not committed. `./jmh/run-jmh.sh` writes `output/07-jmh.txt`. + +Everything except the JMH numbers is byte-stable. The JMH numbers are machine dependent: treat them as shape, not results. + +Tested on Temurin 25.0.4.1+1, 26.0.2.1+1 and 27+35, and OpenJDK 21.0.10 for the plain-Java baseline. diff --git a/lazy-constants/broken/NoPreviewFlag.java b/lazy-constants/broken/NoPreviewFlag.java new file mode 100644 index 0000000..9923b93 --- /dev/null +++ b/lazy-constants/broken/NoPreviewFlag.java @@ -0,0 +1,4 @@ +public class NoPreviewFlag { + // Compiled without --enable-preview. + static final LazyConstant CONFIG = LazyConstant.of(() -> "x"); +} diff --git a/lazy-constants/broken/ReassignInSupplier.java b/lazy-constants/broken/ReassignInSupplier.java new file mode 100644 index 0000000..b64800f --- /dev/null +++ b/lazy-constants/broken/ReassignInSupplier.java @@ -0,0 +1,7 @@ +public class ReassignInSupplier { + public static void main(String[] args) { + // A LazyConstant has no set method: the only way to give it a value is the supplier passed to of(). + LazyConstant c = LazyConstant.of(() -> "x"); + c.set("y"); + } +} diff --git a/lazy-constants/broken/RemovedApi.java b/lazy-constants/broken/RemovedApi.java new file mode 100644 index 0000000..25f4615 --- /dev/null +++ b/lazy-constants/broken/RemovedApi.java @@ -0,0 +1,7 @@ +public class RemovedApi { + public static void main(String[] args) { + LazyConstant c = LazyConstant.of(() -> "x"); + System.out.println(c.isInitialized()); // exists on JDK 26, removed in 27 + System.out.println(c.orElse("fallback")); // exists on JDK 26, removed in 27 + } +} diff --git a/lazy-constants/broken/StillStable.java b/lazy-constants/broken/StillStable.java new file mode 100644 index 0000000..cf557fd --- /dev/null +++ b/lazy-constants/broken/StillStable.java @@ -0,0 +1,4 @@ +public class StillStable { + // JDK 25 preview code, unchanged: StableValue was replaced by LazyConstant. + static final StableValue CONFIG = StableValue.of(); +} diff --git a/lazy-constants/jmh/.gitignore b/lazy-constants/jmh/.gitignore new file mode 100644 index 0000000..c3af857 --- /dev/null +++ b/lazy-constants/jmh/.gitignore @@ -0,0 +1 @@ +lib/ diff --git a/lazy-constants/jmh/fetch.sh b/lazy-constants/jmh/fetch.sh new file mode 100755 index 0000000..3601956 --- /dev/null +++ b/lazy-constants/jmh/fetch.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +# Downloads JMH 1.37 and its two runtime dependencies from Maven Central into ./lib, checking each against the published .sha1 +set -euo pipefail +cd "$(dirname "$0")"; mkdir -p lib +M=https://repo1.maven.org/maven2 +for a in org/openjdk/jmh/jmh-core/1.37/jmh-core-1.37 \ + org/openjdk/jmh/jmh-generator-annprocess/1.37/jmh-generator-annprocess-1.37 \ + net/sf/jopt-simple/jopt-simple/5.0.4/jopt-simple-5.0.4 \ + org/apache/commons/commons-math3/3.6.1/commons-math3-3.6.1; do + f="lib/$(basename "$a").jar" + for try in 1 2 3 4 5; do [ -s "$f" ] && break; curl -sfL -o "$f" "$M/$a.jar" || { rm -f "$f"; sleep 5; }; done + want=""; for try in 1 2 3 4 5; do want="$(curl -sfL "$M/$a.jar.sha1" | cut -d' ' -f1)" && [ -n "$want" ] && break; sleep 5; done + got="$(sha1sum "$f" | cut -d' ' -f1)" + [ "$want" = "$got" ] && echo "ok $f $got" || { echo "SHA1 MISMATCH $f"; exit 1; } +done diff --git a/lazy-constants/jmh/run-jmh.sh b/lazy-constants/jmh/run-jmh.sh new file mode 100755 index 0000000..ff44d9b --- /dev/null +++ b/lazy-constants/jmh/run-jmh.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +# Builds and runs the JMH read-path benchmark on JDK 27 and writes ../output/07-jmh.txt (about 3 minutes). +# JDK27=/path ./run-jmh.sh (run ./fetch.sh once first) +set -uo pipefail +unset JAVA_TOOL_OPTIONS +HERE="$(cd "$(dirname "$0")" && pwd)"; cd "$HERE" +JDK27="${JDK27:-/opt/jdks/jdk27}" +CP="$(ls lib/*.jar | tr '\n' ':')"; B="$(mktemp -d)"; trap 'rm -rf "$B"' EXIT +"$JDK27/bin/javac" --enable-preview --release 27 -cp "$CP" -processorpath "$CP" -d "$B" src/bench/AccessBench.java 2>&1 | grep -v '^Note:' +{ + echo "\$ java --enable-preview org.openjdk.jmh.Main -f 3 -wi 3 -w 1 -i 5 -r 1 ($("$JDK27/bin/java" -version 2>&1 | sed -n 2p | sed 's/.*(build \(.*\))/\1/'), JMH 1.37)" + echo "machine: $(nproc) CPUs, $(grep -m1 'model name' /proc/cpuinfo | sed 's/.*: //')" + "$JDK27/bin/java" --enable-preview -cp "$B:$CP" org.openjdk.jmh.Main -f 3 -wi 3 -w 1 -i 5 -r 1 -jvmArgs "--enable-preview" 2>&1 | sed -n '/^Benchmark /,$p' +} > "$HERE/../output/07-jmh.txt" +cat "$HERE/../output/07-jmh.txt" diff --git a/lazy-constants/jmh/src/bench/AccessBench.java b/lazy-constants/jmh/src/bench/AccessBench.java new file mode 100644 index 0000000..26fb876 --- /dev/null +++ b/lazy-constants/jmh/src/bench/AccessBench.java @@ -0,0 +1,52 @@ +package bench; + +import org.openjdk.jmh.annotations.*; +import java.util.concurrent.TimeUnit; + +/** + * Cost of READING an already-initialised lazily-built value, six ways. Every variant returns k + x, where x is an + * ordinary mutable field, so the JIT cannot fold the whole method away, but can fold a constant k. + */ +@State(Scope.Benchmark) +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +public class AccessBench { + record Config(int k) {} + + // 1. Eager static final: the reference point, always initialised, always foldable. + static final Config EAGER = new Config(7); + + // 2. Holder idiom. + static class Holder { static final Config VALUE = new Config(7); } + + // 3. Double-checked locking with a volatile field. + static volatile Config dclField; + static Config dcl() { + Config local = dclField; + if (local == null) { + synchronized (AccessBench.class) { + local = dclField; + if (local == null) dclField = local = new Config(7); + } + } + return local; + } + + // 4. LazyConstant in a static final field: the case the JEP is designed for. + static final LazyConstant LAZY_STATIC = LazyConstant.of(() -> new Config(7)); + + // 5. LazyConstant in a static field that is NOT final. + static LazyConstant lazyNonFinalStatic = LazyConstant.of(() -> new Config(7)); + + // 6. LazyConstant in an ordinary final instance field. + final LazyConstant lazyInstance = LazyConstant.of(() -> new Config(7)); + + int x = 3; + + @Benchmark public int eagerStaticFinal() { return EAGER.k() + x; } + @Benchmark public int holderIdiom() { return Holder.VALUE.k() + x; } + @Benchmark public int doubleCheckedLocking() { return dcl().k() + x; } + @Benchmark public int lazyConstantStaticFinal() { return LAZY_STATIC.get().k() + x; } + @Benchmark public int lazyConstantNonFinalStatic() { return lazyNonFinalStatic.get().k() + x; } + @Benchmark public int lazyConstantInstanceField() { return lazyInstance.get().k() + x; } +} diff --git a/lazy-constants/output/01-old-ways.txt b/lazy-constants/output/01-old-ways.txt new file mode 100644 index 0000000..368cb4b --- /dev/null +++ b/lazy-constants/output/01-old-ways.txt @@ -0,0 +1,29 @@ +$ javac src/OldWays.java && java OldWays (21.0.10+7-Ubuntu-124.04) + [init] building Expensive via eager static final +main starts (the eager value already exists) +first holder() call: + [init] building Expensive via holder idiom +second holder() call: +first doubleChecked() call: + [init] building Expensive via double-checked locking +second doubleChecked() call: + +$ javac src/OldWays.java && java OldWays (25.0.4.1+1-LTS) + [init] building Expensive via eager static final +main starts (the eager value already exists) +first holder() call: + [init] building Expensive via holder idiom +second holder() call: +first doubleChecked() call: + [init] building Expensive via double-checked locking +second doubleChecked() call: + +$ javac src/OldWays.java && java OldWays (27+35) + [init] building Expensive via eager static final +main starts (the eager value already exists) +first holder() call: + [init] building Expensive via holder idiom +second holder() call: +first doubleChecked() call: + [init] building Expensive via double-checked locking +second doubleChecked() call: diff --git a/lazy-constants/output/02-basics.txt b/lazy-constants/output/02-basics.txt new file mode 100644 index 0000000..70ee115 --- /dev/null +++ b/lazy-constants/output/02-basics.txt @@ -0,0 +1,20 @@ +$ javac --enable-preview --release 27 src/Basics.java && java --enable-preview Basics (27+35) +supplier calls before get: 0 +get() returns: loaded +get() again: loaded +supplier calls after two gets: 1 +16 threads asked at once, supplier calls: 1 + +$ javac --enable-preview --release 27 src/ToStringStates.java && java --enable-preview ToStringStates (27+35) +before get: [computing function=ToStringStates$$Lambda] +after get: [loaded] +after failed get: [failed with=java.lang.IllegalStateException] + +$ javac --enable-preview --release 27 src/Service.java && java --enable-preview Service (27+35) +service created + [load] reading configuration +url: https://example.test +url again: https://example.test + [compile] building the e-mail pattern +isEmail(ankur@example.com): true +isEmail(nope): false diff --git a/lazy-constants/output/03-failures.txt b/lazy-constants/output/03-failures.txt new file mode 100644 index 0000000..664b630 --- /dev/null +++ b/lazy-constants/output/03-failures.txt @@ -0,0 +1,13 @@ +$ javac --enable-preview --release 26 src/Failures.java && java --enable-preview Failures (26.0.2.1+1) +get #1: java.lang.IllegalStateException: boom | cause=null | supplier calls=1 +get #2: java.lang.IllegalStateException: boom | cause=null | supplier calls=2 +get #3: java.lang.IllegalStateException: boom | cause=null | supplier calls=3 +null result: java.lang.NullPointerException | cause=null +recursion: java.lang.IllegalStateException: Recursive invocation of a LazyConstant's computing function: Failures$$Lambda | cause=null + +$ javac --enable-preview --release 27 src/Failures.java && java --enable-preview Failures (27+35) +get #1: java.util.NoSuchElementException: Unable to access the constant because java.lang.IllegalStateException was thrown at initial computation | cause=java.lang.IllegalStateException: boom | supplier calls=1 +get #2: java.util.NoSuchElementException: Unable to access the constant because java.lang.IllegalStateException was thrown at initial computation | cause=null | supplier calls=1 +get #3: java.util.NoSuchElementException: Unable to access the constant because java.lang.IllegalStateException was thrown at initial computation | cause=null | supplier calls=1 +null result: java.util.NoSuchElementException: Unable to access the constant because java.lang.NullPointerException was thrown at initial computation | cause=java.lang.NullPointerException +recursion: java.util.NoSuchElementException: Unable to access the constant because java.lang.IllegalStateException was thrown at initial computation | cause=java.lang.IllegalStateException: Recursive invocation of a LazyConstant's computing function: Failures$$Lambda diff --git a/lazy-constants/output/04-collections.txt b/lazy-constants/output/04-collections.txt new file mode 100644 index 0000000..b921c8a --- /dev/null +++ b/lazy-constants/output/04-collections.txt @@ -0,0 +1,26 @@ +$ javac --enable-preview --release 27 src/LazyCollections.java && java --enable-preview LazyCollections (27+35) +list created, size 3 (nothing computed yet) + computing list element 1 +list.get(1) = v1 +list.get(1) again = v1 +map created, size 3 (nothing computed yet) + computing map value for bb +map.get("bb") = 2 +map.get("bb") again = 2 +set created + testing 2 +set.contains(2) = true + testing 1 +set.contains(1) = false +set.contains(9) = false + testing 3 +set.size() = 2 + +$ javac --enable-preview --release 26 src/LazyCollections.java (26.0.2.1+1) +src/LazyCollections.java:18: error: cannot find symbol + Set set = Set.ofLazy(Set.of(1, 2, 3), n -> { System.out.println(" testing " + n); return n > 1; }); + ^ + symbol: method ofLazy(Set,(n)->{ Sys[...] 1; }) + location: interface Set +1 error +exit=1 diff --git a/lazy-constants/output/05-api-across-releases.txt b/lazy-constants/output/05-api-across-releases.txt new file mode 100644 index 0000000..d31b64c --- /dev/null +++ b/lazy-constants/output/05-api-across-releases.txt @@ -0,0 +1,118 @@ +$ javap java.lang.StableValue (25.0.4.1+1-LTS) +Compiled from "StableValue.java" +public interface java.lang.StableValue { + public abstract boolean trySet(T); + public abstract T orElse(T); + public abstract T orElseThrow(); + public abstract boolean isSet(); + public abstract T orElseSet(java.util.function.Supplier); + public abstract void setOrThrow(T); + public abstract boolean equals(java.lang.Object); + public abstract int hashCode(); + public static java.lang.StableValue of(); + public static java.lang.StableValue of(T); + public static java.util.function.Supplier supplier(java.util.function.Supplier); + public static java.util.function.IntFunction intFunction(int, java.util.function.IntFunction); + public static java.util.function.Function function(java.util.Set, java.util.function.Function); + public static java.util.List list(int, java.util.function.IntFunction); + public static java.util.Map map(java.util.Set, java.util.function.Function); +} + +$ javap java.lang.LazyConstant (26.0.2.1+1) +Compiled from "LazyConstant.java" +public interface java.lang.LazyConstant extends java.util.function.Supplier { + public abstract T orElse(T); + public abstract T get(); + public abstract boolean isInitialized(); + public abstract boolean equals(java.lang.Object); + public abstract int hashCode(); + public abstract java.lang.String toString(); + public static java.lang.LazyConstant of(java.util.function.Supplier); +} + +$ javap java.lang.LazyConstant (27+35) +Compiled from "LazyConstant.java" +public interface java.lang.LazyConstant extends java.util.function.Supplier { + public abstract T get(); + public abstract boolean equals(java.lang.Object); + public abstract int hashCode(); + public abstract java.lang.String toString(); + public static java.lang.LazyConstant of(java.util.function.Supplier); +} + +$ javap java.util.List | grep ofLazy (26.0.2.1+1) + public static java.util.List ofLazy(int, java.util.function.IntFunction); +$ javap java.util.Map | grep ofLazy (26.0.2.1+1) + public static java.util.Map ofLazy(java.util.Set, java.util.function.Function); +$ javap java.util.Set | grep ofLazy (26.0.2.1+1) + +$ javap java.util.List | grep ofLazy (27+35) + public static java.util.List ofLazy(int, java.util.function.IntFunction); +$ javap java.util.Map | grep ofLazy (27+35) + public static java.util.Map ofLazy(java.util.Set, java.util.function.Function); +$ javap java.util.Set | grep ofLazy (27+35) + public static java.util.Set ofLazy(java.util.Set, java.util.function.Predicate); + +$ javac --enable-preview --release 25 src/StableValue25.java && java --enable-preview StableValue25 (25.0.4.1+1-LTS) + [load] configuration +CONFIG.get(): loaded +CONFIG.get(): loaded + [compute] orElseSet +lazy(): computed +lazy(): computed + [element] 2 +LIST.get(2): v2 + +$ javac --enable-preview --release 26 broken/StillStable.java (26.0.2.1+1) +broken/StillStable.java:3: error: cannot find symbol + static final StableValue CONFIG = StableValue.of(); + ^ + symbol: class StableValue + location: class StillStable +broken/StillStable.java:3: error: cannot find symbol + static final StableValue CONFIG = StableValue.of(); + ^ + symbol: variable StableValue + location: class StillStable +2 errors +exit=1 + +$ javac --enable-preview --release 27 broken/StillStable.java (27+35) +broken/StillStable.java:3: error: cannot find symbol + static final StableValue CONFIG = StableValue.of(); + ^ + symbol: class StableValue + location: class StillStable +broken/StillStable.java:3: error: cannot find symbol + static final StableValue CONFIG = StableValue.of(); + ^ + symbol: variable StableValue + location: class StillStable +2 errors +exit=1 + +$ javac --enable-preview --release 26 broken/RemovedApi.java (26.0.2.1+1) +exit=0 + +$ javac --enable-preview --release 27 broken/RemovedApi.java (27+35) +broken/RemovedApi.java:4: error: cannot find symbol + System.out.println(c.isInitialized()); // exists on JDK 26, removed in 27 + ^ + symbol: method isInitialized() + location: variable c of type LazyConstant +broken/RemovedApi.java:5: error: cannot find symbol + System.out.println(c.orElse("fallback")); // exists on JDK 26, removed in 27 + ^ + symbol: method orElse(String) + location: variable c of type LazyConstant +2 errors +exit=1 + +$ javac --enable-preview --release 27 broken/ReassignInSupplier.java (27+35) +broken/ReassignInSupplier.java:5: error: cannot find symbol + c.set("y"); + ^ + symbol: method set(String) + location: variable c of type LazyConstant +1 error +exit=1 diff --git a/lazy-constants/output/06-preview-flags.txt b/lazy-constants/output/06-preview-flags.txt new file mode 100644 index 0000000..db8a8a0 --- /dev/null +++ b/lazy-constants/output/06-preview-flags.txt @@ -0,0 +1,23 @@ +$ javac broken/NoPreviewFlag.java (27+35) +broken/NoPreviewFlag.java:3: error: LazyConstant is a preview API and is disabled by default. + static final LazyConstant CONFIG = LazyConstant.of(() -> "x"); + ^ + (use --enable-preview to enable preview APIs) +broken/NoPreviewFlag.java:3: error: LazyConstant is a preview API and is disabled by default. + static final LazyConstant CONFIG = LazyConstant.of(() -> "x"); + ^ + (use --enable-preview to enable preview APIs) +2 errors +exit=1 + +$ javap -v Basics | grep 'major\|minor' (27+35, compiled with --enable-preview --release 27) + minor version: 65535 + major version: 71 + +$ java Basics (27+35, without --enable-preview) +Error: LinkageError occurred while loading main class Basics + java.lang.UnsupportedClassVersionError: Preview features are not enabled for Basics (class file version 71.65535). Try running with '--enable-preview' + +$ java --enable-preview Basics (26.0.2.1+1, class file built by 27) +Error: LinkageError occurred while loading main class Basics + java.lang.UnsupportedClassVersionError: Basics has been compiled by a more recent version of the Java Runtime (class file version 71.65535), this version of the Java Runtime only recognizes class file versions up to 70.0 diff --git a/lazy-constants/output/07-jmh.txt b/lazy-constants/output/07-jmh.txt new file mode 100644 index 0000000..8856e8f --- /dev/null +++ b/lazy-constants/output/07-jmh.txt @@ -0,0 +1,9 @@ +$ java --enable-preview org.openjdk.jmh.Main -f 3 -wi 3 -w 1 -i 5 -r 1 (27+35, JMH 1.37) +machine: 2 CPUs, Intel(R) Xeon(R) Processor @ 2.80GHz +Benchmark Mode Cnt Score Error Units +AccessBench.doubleCheckedLocking avgt 15 0.964 ± 0.028 ns/op +AccessBench.eagerStaticFinal avgt 15 0.648 ± 0.018 ns/op +AccessBench.holderIdiom avgt 15 0.639 ± 0.024 ns/op +AccessBench.lazyConstantInstanceField avgt 15 1.465 ± 0.065 ns/op +AccessBench.lazyConstantNonFinalStatic avgt 15 1.494 ± 0.064 ns/op +AccessBench.lazyConstantStaticFinal avgt 15 0.636 ± 0.019 ns/op diff --git a/lazy-constants/run.sh b/lazy-constants/run.sh new file mode 100755 index 0000000..2c3dfeb --- /dev/null +++ b/lazy-constants/run.sh @@ -0,0 +1,62 @@ +#!/usr/bin/env bash +# Lazy Constants (JEP 531, third preview in JDK 27) against the holder idiom and double-checked locking. +# Regenerates every file in output/. JDK25=/path JDK26=/path JDK27=/path [JDK21=/path] ./run.sh +set -uo pipefail +unset JAVA_TOOL_OPTIONS +HERE="$(cd "$(dirname "$0")" && pwd)" +JDK25="${JDK25:-/opt/jdks/jdk-25.0.4.1+1}"; JDK26="${JDK26:-/opt/jdks/jdk-26.0.2.1+1}"; JDK27="${JDK27:-/opt/jdks/jdk27}"; JDK21="${JDK21:-}" +OUT="$HERE/output"; mkdir -p "$OUT"; B="$(mktemp -d)"; trap 'rm -rf "$B"' EXIT +cd "$HERE" +t() { "$1/bin/java" -version 2>&1 | sed -n 2p | sed 's/.*(build \(.*\))/\1/'; } +rel() { case "$1" in "$JDK25") echo 25;; "$JDK26") echo 26;; "$JDK27") echo 27;; *) echo 21;; esac; } +mk() { mktemp -d -p "$B"; } +# plain compile + run +cr() { local jdk="$1" src="$2" cls="$3" o; o="$(mk)" + echo "\$ javac $src && java $cls ($(t "$jdk"))" + "$jdk/bin/javac" -d "$o" "$src" 2>&1 && "$jdk/bin/java" -cp "$o" "$cls" 2>&1; } +# preview compile + run +crp() { local jdk="$1" src="$2" cls="$3" o r; o="$(mk)"; r="$(rel "$jdk")" + echo "\$ javac --enable-preview --release $r $src && java --enable-preview $cls ($(t "$jdk"))" + "$jdk/bin/javac" --enable-preview --release "$r" -d "$o" "$src" 2>&1 | grep -v '^Note:' + "$jdk/bin/java" --enable-preview -cp "$o" "$cls" 2>&1; } +# preview compile only +cop() { local jdk="$1" src="$2" o r; o="$(mk)"; r="$(rel "$jdk")" + echo "\$ javac --enable-preview --release $r $src ($(t "$jdk"))" + "$jdk/bin/javac" --enable-preview --release "$r" -d "$o" "$src" 2>&1 | grep -v '^Note:'; echo "exit=${PIPESTATUS[0]}"; } +# compile only, no flags +co() { local jdk="$1" src="$2" o; o="$(mk)" + echo "\$ javac $src ($(t "$jdk"))" + "$jdk/bin/javac" -d "$o" "$src" 2>&1 | grep -v '^Note:'; echo "exit=${PIPESTATUS[0]}"; } +all3() { local fn="$1"; shift; [ -n "$JDK21" ] && { "$fn" "$JDK21" "$@"; echo; }; "$fn" "$JDK25" "$@"; echo; "$fn" "$JDK27" "$@"; } + +# 01 - the classic idioms: when does each one build the value? +{ all3 cr src/OldWays.java OldWays; } > "$OUT/01-old-ways.txt" + +# 02 - LazyConstant basics on 27 +{ crp "$JDK27" src/Basics.java Basics; echo; crp "$JDK27" src/ToStringStates.java ToStringStates; echo; crp "$JDK27" src/Service.java Service; } > "$OUT/02-basics.txt" + +# 03 - failure semantics changed between 26 and 27 +{ crp "$JDK26" src/Failures.java Failures; echo; crp "$JDK27" src/Failures.java Failures; } > "$OUT/03-failures.txt" + +# 04 - lazy collections +{ crp "$JDK27" src/LazyCollections.java LazyCollections; echo; cop "$JDK26" src/LazyCollections.java; } > "$OUT/04-collections.txt" + +# 05 - the API across 25, 26 and 27 +api() { local jdk="$1" cls="$2"; echo "\$ javap $cls ($(t "$jdk"))"; "$jdk/bin/javap" "$cls" 2>&1; } +{ api "$JDK25" java.lang.StableValue; echo; api "$JDK26" java.lang.LazyConstant; echo; api "$JDK27" java.lang.LazyConstant; echo + for j in "$JDK26" "$JDK27"; do for c in List Map Set; do echo "\$ javap java.util.$c | grep ofLazy ($(t "$j"))"; "$j/bin/javap" java.util.$c 2>&1 | grep ofLazy; done; echo; done + crp "$JDK25" src/StableValue25.java StableValue25; echo + cop "$JDK26" broken/StillStable.java; echo; cop "$JDK27" broken/StillStable.java; echo + cop "$JDK26" broken/RemovedApi.java; echo; cop "$JDK27" broken/RemovedApi.java; echo + cop "$JDK27" broken/ReassignInSupplier.java; } > "$OUT/05-api-across-releases.txt" + +# 06 - preview flags and class files +{ co "$JDK27" broken/NoPreviewFlag.java; echo + o="$(mk)"; "$JDK27/bin/javac" --enable-preview --release 27 -d "$o" src/Basics.java 2>&1 | grep -v '^Note:' + echo "\$ javap -v Basics | grep 'major\|minor' (27+35, compiled with --enable-preview --release 27)"; "$JDK27/bin/javap" -v -cp "$o" Basics | grep 'major\|minor'; echo + echo "\$ java Basics (27+35, without --enable-preview)"; "$JDK27/bin/java" -cp "$o" Basics 2>&1 | head -2; echo + echo "\$ java --enable-preview Basics ($(t "$JDK26"), class file built by 27)"; "$JDK26/bin/java" --enable-preview -cp "$o" Basics 2>&1 | head -2; } > "$OUT/06-preview-flags.txt" + +# 07 - JMH read-path benchmark (about 3 minutes; needs jmh/fetch.sh first; set RUN_JMH=0 to skip) +if [ "${RUN_JMH:-1}" = 1 ] && [ -f jmh/lib/jmh-core-1.37.jar ]; then JDK27="$JDK27" jmh/run-jmh.sh > /dev/null; else echo "skipped 07-jmh.txt"; fi +echo done diff --git a/lazy-constants/src/Basics.java b/lazy-constants/src/Basics.java new file mode 100644 index 0000000..d6e5659 --- /dev/null +++ b/lazy-constants/src/Basics.java @@ -0,0 +1,35 @@ +import java.util.ArrayList; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.atomic.AtomicInteger; + +// LazyConstant (JEP 531, third preview in JDK 27). Compile and run with --enable-preview. +public class Basics { + static final AtomicInteger calls = new AtomicInteger(); + + // The supplier runs at most once, on the first get(), no matter how many threads ask. + static final LazyConstant CONFIG = LazyConstant.of(() -> { + calls.incrementAndGet(); + return "loaded"; + }); + + public static void main(String[] args) throws Exception { + System.out.println("supplier calls before get: " + calls.get()); + System.out.println("get() returns: " + CONFIG.get()); + System.out.println("get() again: " + CONFIG.get()); + System.out.println("supplier calls after two gets: " + calls.get()); + + AtomicInteger slowCalls = new AtomicInteger(); + LazyConstant slow = LazyConstant.of(() -> { + slowCalls.incrementAndGet(); + try { Thread.sleep(50); } catch (InterruptedException e) { throw new RuntimeException(e); } + return 42; + }); + var pool = Executors.newFixedThreadPool(16); + var futures = new ArrayList>(); + for (int i = 0; i < 16; i++) futures.add(pool.submit(slow::get)); + for (var f : futures) f.get(); + pool.shutdown(); + System.out.println("16 threads asked at once, supplier calls: " + slowCalls.get()); + } +} diff --git a/lazy-constants/src/Failures.java b/lazy-constants/src/Failures.java new file mode 100644 index 0000000..b19dbea --- /dev/null +++ b/lazy-constants/src/Failures.java @@ -0,0 +1,25 @@ +import java.util.concurrent.atomic.AtomicInteger; + +// What happens when the supplier throws, returns null or asks for its own value. +// The source compiles on JDK 26 and 27; the behaviour is different (see output/03). +public class Failures { + // Lambda class names carry an address that changes on every run; keep the transcript stable. + static String s(Object o) { return String.valueOf(o).replaceAll("\\$\\$Lambda/0x[0-9a-f]+@[0-9a-f]+", "\\$\\$Lambda"); } + + public static void main(String[] args) { + AtomicInteger calls = new AtomicInteger(); + LazyConstant bad = LazyConstant.of(() -> { calls.incrementAndGet(); throw new IllegalStateException("boom"); }); + for (int i = 0; i < 3; i++) { + try { bad.get(); } + catch (Throwable t) { System.out.println("get #" + (i + 1) + ": " + s(t) + " | cause=" + s(t.getCause()) + " | supplier calls=" + calls); } + } + + try { LazyConstant.of(() -> (String) null).get(); } + catch (Throwable t) { System.out.println("null result: " + s(t) + " | cause=" + s(t.getCause())); } + + @SuppressWarnings("unchecked") LazyConstant[] self = new LazyConstant[1]; + self[0] = LazyConstant.of(() -> self[0].get()); + try { self[0].get(); } + catch (Throwable t) { System.out.println("recursion: " + s(t) + " | cause=" + s(t.getCause())); } + } +} diff --git a/lazy-constants/src/LazyCollections.java b/lazy-constants/src/LazyCollections.java new file mode 100644 index 0000000..0111a9a --- /dev/null +++ b/lazy-constants/src/LazyCollections.java @@ -0,0 +1,25 @@ +import java.util.List; +import java.util.Map; +import java.util.Set; + +// List.ofLazy, Map.ofLazy (JDK 26 and 27) and Set.ofLazy (new in JDK 27): each element is computed on first access. +public class LazyCollections { + public static void main(String[] args) { + List list = List.ofLazy(3, i -> { System.out.println(" computing list element " + i); return "v" + i; }); + System.out.println("list created, size " + list.size() + " (nothing computed yet)"); + System.out.println("list.get(1) = " + list.get(1)); + System.out.println("list.get(1) again = " + list.get(1)); + + Map map = Map.ofLazy(Set.of("a", "bb", "ccc"), k -> { System.out.println(" computing map value for " + k); return k.length(); }); + System.out.println("map created, size " + map.size() + " (nothing computed yet)"); + System.out.println("map.get(\"bb\") = " + map.get("bb")); + System.out.println("map.get(\"bb\") again = " + map.get("bb")); + + Set set = Set.ofLazy(Set.of(1, 2, 3), n -> { System.out.println(" testing " + n); return n > 1; }); + System.out.println("set created"); + System.out.println("set.contains(2) = " + set.contains(2)); + System.out.println("set.contains(1) = " + set.contains(1)); + System.out.println("set.contains(9) = " + set.contains(9)); + System.out.println("set.size() = " + set.size()); + } +} diff --git a/lazy-constants/src/OldWays.java b/lazy-constants/src/OldWays.java new file mode 100644 index 0000000..1bd8c27 --- /dev/null +++ b/lazy-constants/src/OldWays.java @@ -0,0 +1,35 @@ +// The three classic ways to hold a value that is expensive to build. Plain Java, no preview features. +public class OldWays { + static class Expensive { + final String how; + Expensive(String how) { this.how = how; System.out.println(" [init] building Expensive via " + how); } + } + + // 1. Eager: built when OldWays is initialised, which is before main runs, whether or not anyone uses it. + static final Expensive EAGER = new Expensive("eager static final"); + + // 2. Holder idiom: the JVM initialises Holder the first time it is touched, and does so exactly once. + static class Holder { static final Expensive VALUE = new Expensive("holder idiom"); } + static Expensive holder() { return Holder.VALUE; } + + // 3. Double-checked locking: correct only because the field is volatile. + private static volatile Expensive dcl; + static Expensive doubleChecked() { + Expensive local = dcl; + if (local == null) { + synchronized (OldWays.class) { + local = dcl; + if (local == null) dcl = local = new Expensive("double-checked locking"); + } + } + return local; + } + + public static void main(String[] args) { + System.out.println("main starts (the eager value already exists)"); + System.out.println("first holder() call:"); holder(); + System.out.println("second holder() call:"); holder(); + System.out.println("first doubleChecked() call:"); doubleChecked(); + System.out.println("second doubleChecked() call:"); doubleChecked(); + } +} diff --git a/lazy-constants/src/Service.java b/lazy-constants/src/Service.java new file mode 100644 index 0000000..f9bd946 --- /dev/null +++ b/lazy-constants/src/Service.java @@ -0,0 +1,29 @@ +import java.util.regex.Pattern; + +// Where the holder idiom and double-checked locking used to live: a costly resource inside an ordinary class. +public class Service { + record Config(String url, int retries) { + static Config load() { System.out.println(" [load] reading configuration"); return new Config("https://example.test", 3); } + } + + // A shared, expensive-to-build value: a static final LazyConstant is the replacement for the holder class. + private static final LazyConstant CONFIG = LazyConstant.of(Config::load); + + // A per-instance value that is only sometimes needed: the replacement for a volatile field plus double-checked locking. + private final LazyConstant emailPattern = LazyConstant.of(() -> { + System.out.println(" [compile] building the e-mail pattern"); + return Pattern.compile("[^@\\s]+@[^@\\s]+\\.[a-z]{2,}"); + }); + + boolean isEmail(String s) { return emailPattern.get().matcher(s).matches(); } + static String url() { return CONFIG.get().url(); } + + public static void main(String[] args) { + Service s = new Service(); + System.out.println("service created"); + System.out.println("url: " + url()); + System.out.println("url again: " + url()); + System.out.println("isEmail(ankur@example.com): " + s.isEmail("ankur@example.com")); + System.out.println("isEmail(nope): " + s.isEmail("nope")); + } +} diff --git a/lazy-constants/src/StableValue25.java b/lazy-constants/src/StableValue25.java new file mode 100644 index 0000000..500583b --- /dev/null +++ b/lazy-constants/src/StableValue25.java @@ -0,0 +1,20 @@ +import java.util.List; +import java.util.function.Supplier; + +// The JDK 25 preview API (JEP 502, Stable Values). It does not exist on 26 or 27: see broken/StillStable.java. +public class StableValue25 { + static final Supplier CONFIG = StableValue.supplier(() -> { System.out.println(" [load] configuration"); return "loaded"; }); + + static final StableValue LAZY = StableValue.of(); + static String lazy() { return LAZY.orElseSet(() -> { System.out.println(" [compute] orElseSet"); return "computed"; }); } + + static final List LIST = StableValue.list(3, i -> { System.out.println(" [element] " + i); return "v" + i; }); + + public static void main(String[] args) { + System.out.println("CONFIG.get(): " + CONFIG.get()); + System.out.println("CONFIG.get(): " + CONFIG.get()); + System.out.println("lazy(): " + lazy()); + System.out.println("lazy(): " + lazy()); + System.out.println("LIST.get(2): " + LIST.get(2)); + } +} diff --git a/lazy-constants/src/ToStringStates.java b/lazy-constants/src/ToStringStates.java new file mode 100644 index 0000000..61419df --- /dev/null +++ b/lazy-constants/src/ToStringStates.java @@ -0,0 +1,14 @@ +// What toString() shows in each state. The text after "@hash" is internal, so match on the bracketed part only. +public class ToStringStates { + public static void main(String[] args) { + LazyConstant ok = LazyConstant.of(() -> "loaded"); + System.out.println("before get: " + shape(ok.toString())); + ok.get(); + System.out.println("after get: " + shape(ok.toString())); + + LazyConstant bad = LazyConstant.of(() -> { throw new IllegalStateException("boom"); }); + try { bad.get(); } catch (RuntimeException e) { /* first failure */ } + System.out.println("after failed get: " + shape(bad.toString())); + } + static String shape(String s) { return s.substring(s.indexOf('[')).replaceAll("\\$\\$Lambda/0x[0-9a-f]+@[0-9a-f]+", "\\$\\$Lambda"); } +}