Summary
Issue warnings about uses of deep reflection to mutate final
fields. The warnings aim to prepare developers for a future release that ensures integrity by default by restricting final
field mutation; this makes Java programs safer and potentially faster. Application developers can avoid both current warnings and future restrictions by selectively enabling the ability to mutate final
fields where essential.
Goals
- Prepare the Java ecosystem for a future release that, by default, disallows the mutation of
final
fields by deep reflection. As of that release, application developers will have to explicitly enable the capability to do so at startup. - Align
final
fields in normal classes with the components of record classes, which cannot be mutated by deep reflection. - Allow serialization libraries to continue working with
Serializable
classes, even those withfinal
fields.
Non-Goals
- It is not a goal to deprecate or remove any part of the Java Platform API.
- It is not a goal to prevent the mutation of
final
fields by serialization libraries during deserialization.
Motivation
Java developers rely on final
fields to represent immutable state. Once assigned in a constructor (for final
instance fields) or in a class initializer (for static final
fields), a final
field cannot be reassigned; its value, whether a primitive value or a reference to an object, is immutable. The expectation that a final
field cannot be reassigned in far-flung parts of the program, whether deliberately or accidentally, is often crucial when developers reason about correctness. Furthermore, many classes exist only to represent immutable state, so records were introduced in JDK 16 to provide a concise way to declare a class where all fields are final
, making it easy to reason about correctness.
The expectation that a final
field cannot be reassigned is also important for performance. The more the JVM knows about the behavior of a class, the more optimizations it can apply. For example, being able to trust that final
fields are never reassigned makes it possible for the JVM to perform constant folding, an optimization that elides the need to load a value from memory since the value can instead be embedded in the machine code emitted by the JIT compiler. Constant folding is often the first step in a chain of optimizations that together provide a massive speed-up.
Unfortunately, the expectation that a final
field cannot be reassigned is false. The Java Platform provides a number of APIs that allow final
fields to be reassigned at any time by any code in the program, undermining all reasoning about correctness and invalidating important optimizations. The most prevalent of these APIs is deep reflection. Here is an example that uses deep reflection to mutate a final
field at will:
class C {
final int x;
C() { x = 100; }
}
// Perform deep reflection over C's final field
java.lang.reflect.Field f = C.class.getDeclaredField("x");
f.setAccessible(true);
// Create an object of class C
C obj = new C();
System.out.println(obj.x); // Prints 100
// Mutate the final field in the object
f.set(obj, 200);
System.out.println(obj.x); // Prints 200
f.set(obj, 300);
System.out.println(obj.x); // Prints 300
Accordingly, a final
field is as mutable as a non-final
field. Developers are unable to use final
fields to construct the deeply immutable graphs of objects that would enable the JVM to deliver the best performance optimizations.
It might seem absurd for the Java Platform to provide an API that undermines the meaning of final
. However, after JDK 5 introduced the Java Memory Model that led to widespread use of final
fields, such an API was deemed necessary to support serialization libraries. In retrospect, offering such unconstrained functionality was a poor choice because it sacrificed integrity. When we introduced hidden classes in JDK 15 and record classes in JDK 16, we constrained deep reflection to disallow mutation of final
fields in hidden and record classes.
We constrained deep reflection further when we strongly encapsulated JDK internals in JDK 17. In JDK 24, we started a process to remove methods in sun.misc.Unsafe
that, like deep reflection, allow mutation of final
fields.
Relatively little code mutates final
fields, but the mere existence of APIs for doing so makes it impossible for developers or the JVM to trust the value of any final
field. This compromises safety and performance in all programs. In line with the policy of integrity by default, we plan to enforce the immutability of final
fields so that code cannot use deep reflection to reassign them at will. We will support one special case — serialization libraries that need to mutate final
fields during deserialization — via a limited-purpose API.
Description
In JDK 5 and later releases, you can mutate final
fields via deep reflection (the setAccessible
and set
methods in java.lang.reflect.Field
). In JDK XX, we will restrict deep reflection so that mutating a final
field also causes a warning to be issued at run time by default. It will not be possible to avoid the warning simply by using --add-opens
to enable deep reflection of classes with final
fields.
We refer to restrictions on mutating final
fields as final
field restrictions. We will strengthen the effect of final
field restrictions over time. Rather than issue warnings, a future JDK release will throw exceptions by default when Java code uses deep reflection to mutate final
fields. The intent is to ensure that applications and the Java Platform have integrity by default.
Enabling final
field mutation
Application developers can avoid warnings (and in the future, exceptions) by enabling final
field mutation for selected Java code at startup. Enabling final
field mutation acknowledges the application’s need to mutate final
fields and lifts the final
field restrictions.
Under the policy of integrity by default, it is the application developer (or perhaps deployer, on the advice of the application developer) who enables final
field mutation, not library developers. Library developers who rely on reflection to mutate final
fields should inform their users that they will need to enable final
field mutation using one of the methods below.
To enable final
field mutation by any code on the class path, regardless of where the final
fields are declared, use the following command-line option:
java --enable-final-field-mutation=ALL-UNNAMED ...
To enable final
field mutation by specific modules on the module path, again regardless of where the final
fields are declared, pass a comma-separated list of module names:
java --enable-final-field-mutation=M1,M2 ...
Most application developers who wish to allow final
field mutation will pass --enable-final-field-mutation
directly to the java
launcher in a startup script, but other techniques are available:
- You can pass
--enable-final-field-mutation
to the launcher indirectly, by setting the environment variableJDK_JAVA_OPTIONS
. - You can put
--enable-final-field-mutation
in an argument file that is passed to the launcher by a script or an end user,