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 <[email protected]>
Claude-Session: https://claude.ai/code/session_01TF9JWFvJSNm6HVzswzZU5a
This commit is contained in:
Claude
2026-09-24 12:27:27 +00:00
parent af44e59e7f
commit 814c32b5ce
25 changed files with 606 additions and 0 deletions
+1
View File
@@ -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/`)
+17
View File
@@ -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.
+4
View File
@@ -0,0 +1,4 @@
public class NoPreviewFlag {
// Compiled without --enable-preview.
static final LazyConstant<String> CONFIG = LazyConstant.of(() -> "x");
}
@@ -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<String> c = LazyConstant.of(() -> "x");
c.set("y");
}
}
+7
View File
@@ -0,0 +1,7 @@
public class RemovedApi {
public static void main(String[] args) {
LazyConstant<String> 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
}
}
+4
View File
@@ -0,0 +1,4 @@
public class StillStable {
// JDK 25 preview code, unchanged: StableValue was replaced by LazyConstant.
static final StableValue<String> CONFIG = StableValue.of();
}
+1
View File
@@ -0,0 +1 @@
lib/
+15
View File
@@ -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
+15
View File
@@ -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"
@@ -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<Config> LAZY_STATIC = LazyConstant.of(() -> new Config(7));
// 5. LazyConstant in a static field that is NOT final.
static LazyConstant<Config> lazyNonFinalStatic = LazyConstant.of(() -> new Config(7));
// 6. LazyConstant in an ordinary final instance field.
final LazyConstant<Config> 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; }
}
+29
View File
@@ -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:
+20
View File
@@ -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([email protected]): true
isEmail(nope): false
+13
View File
@@ -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
+26
View File
@@ -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<Integer> set = Set.ofLazy(Set.of(1, 2, 3), n -> { System.out.println(" testing " + n); return n > 1; });
^
symbol: method ofLazy(Set<Integer>,(n)->{ Sys[...] 1; })
location: interface Set
1 error
exit=1
@@ -0,0 +1,118 @@
$ javap java.lang.StableValue (25.0.4.1+1-LTS)
Compiled from "StableValue.java"
public interface java.lang.StableValue<T> {
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<? extends T>);
public abstract void setOrThrow(T);
public abstract boolean equals(java.lang.Object);
public abstract int hashCode();
public static <T> java.lang.StableValue<T> of();
public static <T> java.lang.StableValue<T> of(T);
public static <T> java.util.function.Supplier<T> supplier(java.util.function.Supplier<? extends T>);
public static <R> java.util.function.IntFunction<R> intFunction(int, java.util.function.IntFunction<? extends R>);
public static <T, R> java.util.function.Function<T, R> function(java.util.Set<? extends T>, java.util.function.Function<? super T, ? extends R>);
public static <E> java.util.List<E> list(int, java.util.function.IntFunction<? extends E>);
public static <K, V> java.util.Map<K, V> map(java.util.Set<K>, java.util.function.Function<? super K, ? extends V>);
}
$ javap java.lang.LazyConstant (26.0.2.1+1)
Compiled from "LazyConstant.java"
public interface java.lang.LazyConstant<T> extends java.util.function.Supplier<T> {
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 <T> java.lang.LazyConstant<T> of(java.util.function.Supplier<? extends T>);
}
$ javap java.lang.LazyConstant (27+35)
Compiled from "LazyConstant.java"
public interface java.lang.LazyConstant<T> extends java.util.function.Supplier<T> {
public abstract T get();
public abstract boolean equals(java.lang.Object);
public abstract int hashCode();
public abstract java.lang.String toString();
public static <T> java.lang.LazyConstant<T> of(java.util.function.Supplier<? extends T>);
}
$ javap java.util.List | grep ofLazy (26.0.2.1+1)
public static <E> java.util.List<E> ofLazy(int, java.util.function.IntFunction<? extends E>);
$ javap java.util.Map | grep ofLazy (26.0.2.1+1)
public static <K, V> java.util.Map<K, V> ofLazy(java.util.Set<? extends K>, java.util.function.Function<? super K, ? extends V>);
$ javap java.util.Set | grep ofLazy (26.0.2.1+1)
$ javap java.util.List | grep ofLazy (27+35)
public static <E> java.util.List<E> ofLazy(int, java.util.function.IntFunction<? extends E>);
$ javap java.util.Map | grep ofLazy (27+35)
public static <K, V> java.util.Map<K, V> ofLazy(java.util.Set<? extends K>, java.util.function.Function<? super K, ? extends V>);
$ javap java.util.Set | grep ofLazy (27+35)
public static <E> java.util.Set<E> ofLazy(java.util.Set<? extends E>, java.util.function.Predicate<? super E>);
$ 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<String> CONFIG = StableValue.of();
^
symbol: class StableValue
location: class StillStable
broken/StillStable.java:3: error: cannot find symbol
static final StableValue<String> 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<String> CONFIG = StableValue.of();
^
symbol: class StableValue
location: class StillStable
broken/StillStable.java:3: error: cannot find symbol
static final StableValue<String> 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<String>
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<String>
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<String>
1 error
exit=1
@@ -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<String> 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<String> 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
+9
View File
@@ -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
+62
View File
@@ -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
+35
View File
@@ -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<String> 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<Integer> 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<Future<Integer>>();
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());
}
}
+25
View File
@@ -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<String> 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<String>[] 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())); }
}
}
+25
View File
@@ -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<String> 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<String, Integer> 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<Integer> 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());
}
}
+35
View File
@@ -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();
}
}
+29
View File
@@ -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> 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<Pattern> 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([email protected]): " + s.isEmail("[email protected]"));
System.out.println("isEmail(nope): " + s.isEmail("nope"));
}
}
+20
View File
@@ -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<String> CONFIG = StableValue.supplier(() -> { System.out.println(" [load] configuration"); return "loaded"; });
static final StableValue<String> LAZY = StableValue.of();
static String lazy() { return LAZY.orElseSet(() -> { System.out.println(" [compute] orElseSet"); return "computed"; }); }
static final List<String> 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));
}
}
+14
View File
@@ -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<String> ok = LazyConstant.of(() -> "loaded");
System.out.println("before get: " + shape(ok.toString()));
ok.get();
System.out.println("after get: " + shape(ok.toString()));
LazyConstant<String> 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"); }
}