Hi Protonull,
You know that in a multi-threaded application, your example suffers from
multiple problems, right?
- you "publish" an instance from within its constructor. This does not
guarantee proper memory ordering that would ensure published instance is
observed fully initialized in other threads that may get a hold on it
even though the publishing instruction is the last in the execution of
the constructor.
- you "publish" an instance via normal static field, which again suffers
from possible (data) races which might cause the published instance to
be observed as not fully initialized in other threads and also multiple
instances to be published and used - not just one.
But keeping that aside, it is possible to construct and publish the
instance safely using just the functional style of LazyValue API, where
you keep the appearance that registration is done by custom code. By
combining LazyValue with yet another of the newly added API(s). For example:
public abstract class AbstractExampleMod {
private static final ScopedValue<AbstractExampleMod>
REGISTERING_INSTANCE = ScopedValue.newInstance();
private static final LazyConstant<AbstractExampleMod> LAZY_INSTANCE
= LazyConstant.of(REGISTERING_INSTANCE::get);
public static AbstractExampleMod getInstance() {
return LAZY_INSTANCE.get();
}
protected static void register(AbstractExampleMod instance) {
Objects.requireNonNull(instance);
var registeredInstance = ScopedValue
.where(REGISTERING_INSTANCE, instance)
.call(AbstractExampleMod::getInstance);
if (registeredInstance != instance) {
throw new IllegalStateException("Instance is already
registered!");
}
}
public abstract void sayHello();
}
...in custom code you just do the following then:
public class PlatformExampleMod extends AbstractExampleMod {
static void register() {
register(new PlatformExampleMod());
}
private PlatformExampleMod() {
// ...let the constructor finish without registering the instance
}
@Override
public void sayHello() {
System.out.println("Hello from PlatformExampleMod");
}
static void main() {
PlatformExampleMod.register();
AbstractExampleMod.getInstance().sayHello();
}
}
Regards, Peter
P.S. A non-functional LazyValue initialization API might still be
desirable so one does not have to resort to such tricks to achieve it.
On 3/20/26 2:24 PM, Protonull wrote:
Morning,
After having updated to JDK 26, I have some feedback regarding JEP 526: specifically the
removal of the "low[er]-level methods".
My particular use case is creating mods for a popular block game, which has
quite a few competing mod loaders. These mod loaders generally require you to
specify a class that extend or implement some kind of interface to handle
lifecycle events and so on.
Mod developers who wish to support multiple mod loaders typically have a
"common" project, which defines the mod's behaviour, and then one or more
platform-implementation projects which bridge the gap between common's APIs and the
platform's (such as how to implement the registration of key bindings).
What mod developers typically do is define an abstract singleton which the
platform-impl projects implement and set. Which can look something like the
following:
public abstract class AbstractExampleMod {
protected static AbstractExampleMod instance;
public static AbstractExampleMod getInstance() {
return instance;
}
protected void handleEnable() {
this.registerKeyBinding(new KeyBinding(Key.R));
}
// Example platform-specific method
protected abstract void registerKeyBinding(KeyBinding keyBinding);
}
With a platform-impl class resembling the following:
public class PlatformExampleMod extends AbstractExampleMod implements
IPlatformMod {
public PlatformExampleMod() {
if (instance != null) throw new IllegalStateException("instance is
already set!");
instance = this;
}
@Override // from IPlatformMod
public void onModEnable() {
this.handleEnable();
}
@Override // from AbstractExampleMod
protected void registerKeyBinding(KeyBinding keyBinding) {
Platform.registerKeyBinding(keyBinding);
}
}
Unfortunately, most of the mod loaders do not let you control how or under what
circumstances your mod class is constructed: setting the instance occurs once
you already have the instance. This was not a problem as StableValue had
setOrThrow/trySet, which let you define a placeholder StableValue to set later.
This allowed me to update my mods to take advantage of StableValue while only
making minor code changes, eg:
public abstract class AbstractExampleMod {
public static final StableValue<AbstractExampleMod> instance =
StableValue.of();
// etc
}
public class PlatformExampleMod extends AbstractExampleMod implements
IPlatformMod {
public PlatformExampleMod() {
instance.setOrThrow(this);
}
// etc
}
While this may seem like a niche use case, this could also apply to values set
within a main method, such as setting a particular implementation of an
interface to a static constant based on a command-line argument. However, with
JEP 526 and the removal of the these methods, there seems to be no other option
but to return to the non-final field strategy (or otherwise remaining on JDK 25
preview), which while not the end of the world, is nonetheless rather
unfortunate in my opinion. Would it be at all possible to reinstate these
methods?