Change private static final field using Java reflection

Viewed 319304

I have a class with a private static final field that, unfortunately, I need to change it at run-time.

Using reflection I get this error: java.lang.IllegalAccessException: Can not set static final boolean field

Is there any way to change the value?

Field hack = WarpTransform2D.class.getDeclaredField("USE_HACK");
hack.setAccessible(true);
hack.set(null, true);
14 Answers

Even in spite of being final a field can be modified outside of static initializer and (at least JVM HotSpot) will execute the bytecode perfectly fine.

The problem is that Java compiler does not allow this, but this can be easily bypassed using objectweb.asm. Here is p̶e̶r̶f̶e̶c̶t̶l̶y̶ ̶v̶a̶l̶i̶d̶ ̶c̶l̶a̶s̶s̶f̶i̶l̶e̶ an invalid classfile from the JVMS specification standpoint, but it passes bytecode verification and then is successfully loaded and initialized under JVM HotSpot OpenJDK12:

ClassWriter cw = new ClassWriter(0);
cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "Cl", null, "java/lang/Object", null);
{
    FieldVisitor fv = cw.visitField(Opcodes.ACC_PRIVATE | Opcodes.ACC_STATIC | Opcodes.ACC_FINAL, "fld", "I", null, null);
    fv.visitEnd();
}
{
    // public void setFinalField1() { //... }
    MethodVisitor mv = cw.visitMethod(Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC, "setFinalField1", "()V", null, null);
    mv.visitMaxs(2, 1);
    mv.visitInsn(Opcodes.ICONST_5);
    mv.visitFieldInsn(Opcodes.PUTSTATIC, "Cl", "fld", "I");
    mv.visitInsn(Opcodes.RETURN);
    mv.visitEnd();
}
{
    // public void setFinalField2() { //... }
    MethodVisitor mv = cw.visitMethod(Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC, "setFinalField2", "()V", null, null);
    mv.visitMaxs(2, 1);
    mv.visitInsn(Opcodes.ICONST_2);
    mv.visitFieldInsn(Opcodes.PUTSTATIC, "Cl", "fld", "I");
    mv.visitInsn(Opcodes.RETURN);
    mv.visitEnd();
}
cw.visitEnd();

In Java, the class looks roughly speaking as follows:

public class Cl{
    private static final int fld;

    public static void setFinalField1(){
        fld = 5;
    }

    public static void setFinalField2(){
        fld = 2;
    }
}

which cannot be compiled with javac, but can be loaded and executed by JVM.

JVM HotSpot has special treatment of such classes in the sense that it prevents such "constants" from participating in constant folding. This check is done on the bytecode rewriting phase of class initialization:

// Check if any final field of the class given as parameter is modified
// outside of initializer methods of the class. Fields that are modified
// are marked with a flag. For marked fields, the compilers do not perform
// constant folding (as the field can be changed after initialization).
//
// The check is performed after verification and only if verification has
// succeeded. Therefore, the class is guaranteed to be well-formed.
InstanceKlass* klass = method->method_holder();
u2 bc_index = Bytes::get_Java_u2(bcp + prefix_length + 1);
constantPoolHandle cp(method->constants());
Symbol* ref_class_name = cp->klass_name_at(cp->klass_ref_index_at(bc_index));
if (klass->name() == ref_class_name) {
   Symbol* field_name = cp->name_ref_at(bc_index);
   Symbol* field_sig = cp->signature_ref_at(bc_index);

   fieldDescriptor fd;
   if (klass->find_field(field_name, field_sig, &fd) != NULL) {
      if (fd.access_flags().is_final()) {
         if (fd.access_flags().is_static()) {
            if (!method->is_static_initializer()) {
               fd.set_has_initialized_final_update(true);
            }
          } else {
            if (!method->is_object_initializer()) {
              fd.set_has_initialized_final_update(true);
            }
          }
        }
      }
    }
}

The only restriction that JVM HotSpot checks is that the final field should not be modified outside of the class that the final field is declared at.

With JDK 18 this won't be possible anymore due to the reimplementation of the core reflection over invokedynamic and MethodHandles as part of JEP-416 (PR).

Quote of Mandy Chung – who is the main author of this incredible work – in the following comment. Emphasis are mine.

If the underlying field is final, a Field object has write access if and only if

  • setAccessible(true) has succeeded for this Field object;
  • the field is non-static; and
  • the field's declaring class is not a hidden class; and
  • the field's declaring class is not a record class.

Many of the answers here are useful, but I've found none of them to work on Android, in particular. I'm even a pretty hefty user of Reflect by joor, and neither it nor apache's FieldUtils - both mentioned here in some of the answers, do the trick.

Problem with Android

The fundamental reason why this is so is because on Android there's no modifiers field in the Field class, which renders any suggestion involving this code (as in the marked answer), useless:

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

In fact, to quote from FieldUtils.removeFinalModifier():

// Do all JREs implement Field with a private ivar called "modifiers"?
final Field modifiersField = Field.class.getDeclaredField("modifiers");

So, the answer is no...

Solution

Pretty easy - instead of modifiers, the field name is accessFlags. This does the trick:

Field accessFlagsField = Field.class.getDeclaredField("accessFlags");
accessFlagsField.setAccessible(true);
accessFlagsField.setInt(field, field.getModifiers() & ~Modifier.FINAL);

Side-note #1: this can work regardless of whether the field is static in the class, or not.

Side-note #2: Seeing that the field itself could be private, it's recommended to also enable access over the field itself, using field.setAccessible(true) (in addition to accessFlagsField.setAccessible(true).

Since Java 12 onwards, the answers given will not work.

Here is an example on how to modify a private static final field since Java 12 (based on this answer).

  private Object modifyField(Object newFieldValue, String fieldName, Object classInstance) throws NoSuchFieldException, IllegalAccessException {
    Field field = classInstance.getClass().getDeclaredField(fieldName);
    VarHandle MODIFIERS;

    field.setAccessible(true);

    var lookup = MethodHandles.privateLookupIn(Field.class, MethodHandles.lookup());
    MODIFIERS = lookup.findVarHandle(Field.class, "modifiers", int.class);
    int mods = field.getModifiers();

    if (Modifier.isFinal(mods)) {
      MODIFIERS.set(field, mods & ~Modifier.FINAL);
    }

    Object previousValue = field.get(classInstance);
    field.set(null, newFieldValue);

    return previousValue;
  }

See this thread for more details.

If your field is simply private you can do this:

MyClass myClass= new MyClass();
Field aField= myClass.getClass().getDeclaredField("someField");
aField.setAccessible(true);
aField.set(myClass, "newValueForAString");

and throw/handle NoSuchFieldException

Related