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?

Reply via email to