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:
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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())); }
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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"); }
|
||||
}
|
||||
Reference in New Issue
Block a user