Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Can you set value of a private final variable in Java 21?

This used to be easy in Java 8 and I want to know if it's still possible in Java 21. I've seen a bunch of old posts, all giving different methods for this, none of which worked. I'm hoping for a universal solution, but I'll include details of my use case too.

What do I need it for and what can I control?
I am making a Minecraft plugin - that means my code is in a jar file and is not loaded instantly alongside the server, that happens a few seconds later using a class loader. This being a plugin also means I can't control environment variables or parameters when JVM is started.
I want to change the value of a private final commandMap variable in org.bukkit.plugin.SimplePluginManager class. It's not primitive.

What have I tried?
I cannot use just field.setAccessible(true);, because modern java throws IllegalArgumentException: Can not set final field.
I cannot make the field non-final using modifiers field (code below), since java.lang.reflect.Field fields have been hidden.

field.setAccessible(true);
Field modifiersField = field.getClass().getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);

I tried org.apache.commons.lang3.reflect.FieldUtils only to realize it uses the same trick as above.
I tried using sun.misc.Unsafe from this answer but I don't know how to apply it to a non-static variable. The comments say this works on Java 21.

like image 274
Nathalie Kitty Avatar asked Sep 07 '25 15:09

Nathalie Kitty


1 Answers

Yes, you can change the value of a final field on an object using Unsafe. However, the fact that this is now prevented even through the reflection API has a strong reason: the JVM is allowed to use any optimization it sees fit that relies on the assumption that final fields never change. You can read the full detail and reasoning behind this in JEP 8349536.

Here's some code that proves both that, yes, you can change the field, and no, the JVM won't care unless you use the Reflection API to read the field:

import sun.misc.Unsafe;
import java.lang.reflect.Field;

public class Test {

    private final String x = "x";

    public static void main(String[] args) throws Exception {
        Unsafe unsafe = unsafe();

        Test test = new Test();
        Field field = Test.class.getDeclaredField("x");
        long offset = unsafe.objectFieldOffset(field);
        System.out.println("Before: " + test.x);
        unsafe.putObject(test, offset, "modified");
        System.out.println("After, regular access: " + test.x);
        System.out.println("After, using reflection: " + field.get(test));
    }

    private static Unsafe unsafe() throws Exception {
        // Get Unsafe instance
        Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe");
        unsafeField.setAccessible(true);
        return (Unsafe) unsafeField.get(null);
    }
}

The output on my machine is:

Before: x
After, regular access: x
After, using reflection: modified

I tried out some variations, like making test a volatile variable, using volatile test2 and assigning after the change, using a new thread to read the field. Nothing worked.

You can try the above and maybe it works for you, but it will at best be a brittle and error-prone solution.

I will add that the mere fact that, in this particular code, the change is seen through reflection but not direct access, doesn't tell us anything deep about reflection and final fields. It's just an artifact of optimizations existing for direct field access, and access by reflection goes around them. After the optimization known as accessor inflation kicks in, the JVM will have generated bytecode to directly access the field, and then you may see the same issue again.

like image 188
William F. Jameson Avatar answered Sep 10 '25 04:09

William F. Jameson