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
+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"); }
}