Java
Programming
Reflection
Static Final Field
Software Development

Change private static final field using Java reflection

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Java reflection can modify private static final fields, but the technique depends heavily on which JDK version you are running. In JDK 8 and earlier, you could strip the final modifier through the modifiers field hack. In JDK 9 through 11, the same approach still worked but produced warnings. Starting with JDK 12, the modifiers field was removed from java.lang.reflect.Field, and in JDK 16+, strong encapsulation blocks the workaround entirely by default.

The short answer: this technique works reliably only on older JDKs. On modern Java, you need VarHandle, MethodHandles.Lookup, or --add-opens flags, and even those are increasingly restricted. Understanding the evolution matters because a lot of legacy code and Stack Overflow answers still reference the old approach.

Why Developers Try This

Modifying private static final fields comes up in a few scenarios.

Testing. Overriding a constant (like a timeout, feature flag, or endpoint URL) in unit tests without changing production code.

Legacy codebases. Changing configuration constants in third-party libraries where the author did not expose a setter.

Framework internals. Some frameworks (older versions of Spring, Guice) use reflection to inject values into final fields.

In all cases, the practice is a workaround, not a recommended pattern. If you control the code, expose configuration through constructors or configuration objects instead.

The Classic Approach (JDK 8 and Earlier)

This is the technique most tutorials describe. It works by accessing the modifiers field of java.lang.reflect.Field itself and stripping the FINAL bit.

java
1import java.lang.reflect.Field;
2import java.lang.reflect.Modifier;
3
4public class ReflectionDemo {
5    private static final String CONFIG_URL = "https://default.example.com";
6
7    public static void main(String[] args) throws Exception {
8        System.out.println("Before: " + CONFIG_URL);
9
10        Field field = ReflectionDemo.class.getDeclaredField("CONFIG_URL");
11        field.setAccessible(true);
12
13        // Remove the final modifier
14        Field modifiersField = Field.class.getDeclaredField("modifiers");
15        modifiersField.setAccessible(true);
16        modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);
17
18        // Set the new value (null for static fields)
19        field.set(null, "https://test.example.com");
20
21        System.out.println("After: " + CONFIG_URL);
22    }
23}

On JDK 8, this prints.

text
Before: https://default.example.com
After: https://test.example.com

Step-by-Step Breakdown

StepAPI CallPurpose
1. Get the target fieldgetDeclaredField("CONFIG_URL")Access the field regardless of visibility
2. Make it accessiblefield.setAccessible(true)Bypass private access check
3. Get the modifiers fieldField.class.getDeclaredField("modifiers")Access Field's own internal modifiers int
4. Strip FINALmodifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL)Clear the final bit using bitwise AND with NOT
5. Set new valuefield.set(null, "https://test.example.com")First arg is null because the field is static

The Compile-Time Constant Trap

There is a critical caveat with primitive and String constants. If the JVM can determine the value at compile time, the compiler inlines the constant directly into every call site. Reflection changes the field, but the inlined copies remain unchanged.

java
1public class InlineDemo {
2    private static final int TIMEOUT = 30;         // Compile-time constant
3    private static final Integer TIMEOUT_OBJ = 30; // NOT a compile-time constant
4
5    public static void main(String[] args) throws Exception {
6        // Change TIMEOUT via reflection...
7        // System.out.println(TIMEOUT) still prints 30
8        // because the compiler replaced TIMEOUT with the literal 30
9    }
10}

This applies to String literals, int, long, double, float, boolean, char, short, and byte when assigned a literal or constant expression. The field value changes in the reflective view, but compiled bytecode already has the old value baked in.

To avoid this, the original field must be initialized with a non-constant expression.

java
private static final String CONFIG_URL = System.getenv("DEFAULT_URL");
// This is NOT a compile-time constant, so reflection works as expected

Behavior Across JDK Versions

JDK Versionmodifiers Field HackVarHandle--add-opens RequiredNotes
8 and earlierWorksN/ANoNo warnings
9 to 11Works with warningsAvailable (JDK 9+)No (but recommended)Illegal reflective access warning
12 to 15Fails (NoSuchFieldException)Works with --add-opensYes for cross-module accessmodifiers field removed from Field
16+FailsRestricted by defaultYes, mandatoryStrong encapsulation enforced

Modern Alternatives

Using VarHandle (JDK 9+)

VarHandle provides a cleaner API for field access, though it still requires --add-opens for final fields in modules you do not own.

java
1import java.lang.invoke.MethodHandles;
2import java.lang.invoke.VarHandle;
3import java.lang.reflect.Field;
4
5public class VarHandleDemo {
6    private static final String CONFIG_URL = new String("https://default.example.com");
7
8    public static void main(String[] args) throws Exception {
9        // Requires: --add-opens java.base/java.lang.reflect=ALL-UNNAMED
10        var lookup = MethodHandles.privateLookup(
11            Field.class, MethodHandles.lookup()
12        );
13
14        VarHandle modifiers = lookup.findVarHandle(
15            Field.class, "modifiers", int.class
16        );
17
18        Field field = VarHandleDemo.class.getDeclaredField("CONFIG_URL");
19        field.setAccessible(true);
20
21        int mods = field.getModifiers();
22        modifiers.set(field, mods & ~java.lang.reflect.Modifier.FINAL);
23
24        field.set(null, "https://test.example.com");
25        System.out.println(CONFIG_URL);
26    }
27}

Using --add-opens JVM Flag

When running on JDK 16+, you must pass explicit module-opening flags.

bash
java --add-opens java.base/java.lang.reflect=ALL-UNNAMED \
     --add-opens java.base/java.lang=ALL-UNNAMED \
     VarHandleDemo

Without these flags, the JVM throws InaccessibleObjectException.

Using Test Frameworks Instead

For testing (the most common use case), modern frameworks provide safer alternatives.

java
1// Mockito (static mocking, requires mockito-inline or mockito 5+)
2try (var mock = mockStatic(ConfigClass.class)) {
3    mock.when(ConfigClass::getTimeout).thenReturn(60);
4    // test logic
5}
java
1// Spring Test
2@TestPropertySource(properties = "app.timeout=60")
3class MyServiceTest {
4    // Spring injects the overridden value
5}

These approaches avoid reflection entirely and are not affected by JDK version changes.

When Reflection on Final Fields is Justified

The honest answer is rarely. But there are legitimate cases.

Deserializers and ORM frameworks that reconstruct objects from external data need to set final fields during object construction. Libraries like Gson and Jackson use setAccessible internally for this purpose.

Security research and instrumentation tools that inspect or modify running applications need deep reflective access by design.

Legacy system integration where changing the source code is not an option and no configuration API exists.

In all other cases, redesign the code to accept values through constructors, configuration objects, or dependency injection.

Common Pitfalls

Assuming compile-time constants change at all call sites. The JVM inlines static final primitives and String literals. The field value changes in reflection, but every place the constant was used in compiled code still has the old value.

Testing on JDK 8 and deploying on JDK 17+. The modifiers hack works on 8 but throws NoSuchFieldException on 12+ and InaccessibleObjectException on 16+. Always test reflective code against your production JDK.

Forgetting that field.set(null, value) is for static fields only. For instance fields, pass the object instance as the first argument.

Not restoring original values after tests. If a test modifies a static final field and does not restore it, subsequent tests in the same JVM see the modified value, causing flaky test suites.

Using getField instead of getDeclaredField. The getField method only returns public fields. Private fields require getDeclaredField.

Summary

  • The classic modifiers field hack works on JDK 8 and earlier, produces warnings on JDK 9 to 11, and fails on JDK 12+.
  • Compile-time constants (primitive literals, String literals) are inlined by the compiler. Reflection changes the field but not the inlined copies.
  • On modern JDKs, use VarHandle with --add-opens flags, or better yet, use framework-level solutions like Mockito or Spring's @TestPropertySource.
  • For production code, redesign away from reflecting on final fields. Expose configuration through constructors or configuration objects.
  • Always restore modified fields after tests to prevent cross-test contamination.

Course illustration
Course illustration

All Rights Reserved.