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.
On JDK 8, this prints.
Step-by-Step Breakdown
| Step | API Call | Purpose |
| 1. Get the target field | getDeclaredField("CONFIG_URL") | Access the field regardless of visibility |
| 2. Make it accessible | field.setAccessible(true) | Bypass private access check |
3. Get the modifiers field | Field.class.getDeclaredField("modifiers") | Access Field's own internal modifiers int |
4. Strip FINAL | modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL) | Clear the final bit using bitwise AND with NOT |
| 5. Set new value | field.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.
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.
Behavior Across JDK Versions
| JDK Version | modifiers Field Hack | VarHandle | --add-opens Required | Notes |
| 8 and earlier | Works | N/A | No | No warnings |
| 9 to 11 | Works with warnings | Available (JDK 9+) | No (but recommended) | Illegal reflective access warning |
| 12 to 15 | Fails (NoSuchFieldException) | Works with --add-opens | Yes for cross-module access | modifiers field removed from Field |
| 16+ | Fails | Restricted by default | Yes, mandatory | Strong 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.
Using --add-opens JVM Flag
When running on JDK 16+, you must pass explicit module-opening flags.
Without these flags, the JVM throws InaccessibleObjectException.
Using Test Frameworks Instead
For testing (the most common use case), modern frameworks provide safer alternatives.
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
modifiersfield 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
VarHandlewith--add-opensflags, 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.

