JEP draft: Prepare to Make Final Mean Final
Author | Ron Pressler & Alex Buckley |
Owner | Ron Pressler |
Type | Feature |
Scope | JDK |
Status | Draft |
Component | core-libs |
Discussion | jdk dash dev at openjdk dot org |
Created | 2025/02/06 10:25 |
Updated | 2025/04/02 06:40 |
Issue | 8349536 |
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 implicitly declared fields 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 can provide a significant 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; }
}
// 1. Perform deep reflection over the final field in class C
java.lang.reflect.Field f = C.class.getDeclaredField("x");
f.setAccessible(true); // Make C's final field mutable
// 2. Create an object of class C
C obj = new C();
System.out.println(obj.x); // Prints 100
// 3. 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 so that applications and the Java Platform have integrity by default. The warning is intended to prepare application developers for that future.
Enabling final
field mutation
Application developers can avoid warnings (and in the future, exceptions) by enabling final
field mutation for selected Java code on the command line (or equivalent alternatives). 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 ...
Enabling final
field mutation for a module may make code in the module run more slowly than code in modules where final
field mutation is not enabled. In addition, enabling final
field mutation for a module does not guarantee that code in the module will be able to perform deep reflection to mutate final
fields: any final
field to be mutated must be open to the code performing deep reflection, as detailed in the next section.
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, e.g.,java @config
- You can add
Enable-Reflective-Final-Mutation
to the manifest of an executable JAR file, i.e., a JAR file that is launched viajava -jar
. (The only supported value for theEnable-Reflective-Final-Mutation
manifest entry isALL-UNNAMED
; other values cause an exception to be thrown.) - If you create a custom Java runtime for your application, you can pass the
--enable-final-field-mutation
option tojlink
via the--add-options
option, so that reflective final field mutation is enabled in the resulting runtime image. - The JNI Invocation API allows a native application to embed a JVM in its own process. A native application which uses the JNI Invocation API can enable
final
field mutation for modules in the embedded JVM by passing the--enable-final-field-mutation
option when creating the JVM.
API changes in JDK XX
The behavior of Field::setAccessible
is unchanged. This means that when code calls f.setAccessible(true)
on a Field
object f
, the code must either be in the same module as the field reflected by f
, or, if the code is in a different module, the field reflected by f
must be accessible to the caller via exports
or opens
. The call throws InaccessibleObjectException
if these conditions are not met.
The behavior of Field::set
is changed to have an additional condition:
If the underlying field is final, this Field object has write access if and only if the following conditions are met: setAccessible(true) has succeeded for this Field object; and **final field mutation is enabled for the caller's module and the package of the field reflected by this Field object is open to the caller's module; and** 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. If any of the above checks is not met, this method throws an IllegalAccessException. **Note that if this Field object reflects a field which is declared in a module other than the caller's module, then it is not sufficient for the field to be declared as public in an exported package. The field must be declared in a package that is open.** **Note that the caller might not be the same as the caller of setAccessible(true).**
For reference, package p
in module M
is open to module N
if:
N
isM
itself, orM
'smodule-info
containsopens p
oropens p to N
, orM
is an automatic module, i.e., its classes are placed on the module path but it has nomodule-info
, orM
is an unnamed module (all classes on the class path are in an unnamed module), or- The application was started with the command-line option
--add-opens M/p=N
, or the application was launched as an executable JAR file whose manifest contains an appropriateAdd-Opens
attribute.
Because every module is open to itself, code in a module may use deep reflection to mutate final
fields of any class in the same module, provided that final
field mutation is enabled for that module.
The behavior of other relevant methods is as follows:
-
The behavior of
MethodHandles.Lookup::unreflectSetter
is changed along similar lines asField::set
. -
The
Module::addOpens
method allows a caller in moduleM
to open a package in moduleN
to another moduleO
at run time, provided that the package is already open toM
itself. Calling this method will not enableO
to mutatefinal
fields in the package, even iffinal
field mutation was enabled forO
on the command line, because the JVM already decided to trust thefinal
fields in the package based on it not being open toO
at startup.The same applies to
ModuleLayer.Controller::addOpens
andInstrumentation.redefineModule
. -
The
System::setIn
,System::setOut
, andSystem::setErr
methods exist to mutate, respectively, thefinal
fieldsSystem.in
,System.out
, andSystem.err
. These fields have always been write-protected, which means they can be mutated only by calling the corresponding methods inSystem
. It has never been possible to mutate these fields via deep reflection. In JDK XX, there is no change of any kind to these fields and their corresponding methods.
Controlling the effect of final
field restrictions
If final
field mutation is not enabled for a module then it is illegal for code in the module to mutate any final
field via deep reflection. That is, given a Field
object f
that reflects a final
field, it may be legal for code in the module to call f.setAccessible(true)
(if the field is in a package that is exported or opened to the caller), but it is illegal for code in the module to call f.set(..., ...)
.
If final
field mutation is enabled for a module, but some final
field is in a package that is not opened to the module, then it is illegal for code in the module to mutate that final
field via deep reflection. This scenario can occur when code in one module, to which the field's package is open, calls f.setAccessible(true)
and then passes f
to code in a different module, for which final
field mutation is enabled but to which the field's package is not open. It is illegal for the code that receives f
to call f.set(..., ...)
.
What action the Java runtime takes when an illegal final
field mutation is attempted is controlled by a new command-line option, --illegal-reflective-final-mutation
. This is similar in spirit and form to the --illegal-access
option introduced by JEP 261 in JDK 9 and to --illegal-native-access
introduced by JEP 472 in JDK 24. It works as follows:
-
--illegal-final-field-mutation=allow
allows the mutation to proceed without warning. -
--illegal-final-field-mutation=warn
allows the mutation but issues a warning the first time that illegalfinal
field mutation occurs in a particular module. At most one warning per module is issued.This mode is the default in JDK XX. It will be phased out in a future release and, eventually, removed.
-
--illegal-final-field-mutation=deny
will result inField::set
throwing anIllegalAccessException
for every illegalfinal
field mutation.This mode will become the default in a future release.
When deny
becomes the default mode, allow
will be removed but warn
will remain supported for at least one release.
To prepare for the future, we recommend running existing code with the deny
mode to identify code that mutates final
fields via deep reflection.
Warnings on mutation of final
fields
When Field::set
on a final
field is called from a module for which final
field mutation is not enabled, the mutation will succeed but the Java runtime will, by default, issue a warning that identifies the caller:
WARNING: Final field f in p.C has been [mutated/unreflected for mutation] by class com.foo.Bar.caller in module N (file:/path/to/foo.jar)
WARNING: Use --enable-reflective-final-mutation=N to avoid a warning
WARNING: Mutating final fields will be blocked in a future release unless final field mutation is enabled
At most one such warning is issued for any particular module, and only if a warning has not yet been issued for that module. The warning is written to the standard error stream.
Libraries should not use deep reflection to mutate final
fields
The ability to mutate final
fields via deep reflection was added in JDK 5 so that serialization libraries could provide functionality on par with the JDK's own serialization facilities. In particular, the JDK can deserialize objects from an input stream even if the object's class declares final
fields. The JDK bypasses the class's constructors that ordinarily assign instance fields, and instead assigns values from the input stream to instance fields directly -- even if they are final
. Third-party serialization libraries use deep reflection to do the same.
When final
field restrictions are strengthened in a future JDK release, serialization libraries will no longer be able to use deep reflection out of the box. Rather than asking users to enable final
field mutation on the command line, the developers of serialization libraries should serialize and deserialize objects using the sun.reflect.ReflectionFactory
class, which is supported for this purpose. Its deserialization methods can mutate final
fields even if called from code in modules that are not enabled for final
field mutation.
The sun.reflect.ReflectionFactory
class only supports deserialization of objects whose classes implement java.io.Serializable
. We believe this limitation balances the interests of developers using serialization libraries with the wider interest of all developers in having correct and efficient execution. It ensures that the JVM, when performing optimizations such as constant folding, is not unduly constrained in the assumptions it can make: it must assume that final
fields in Serializable
objects are potentially mutable but can assume that final
fields in all other objects (the vast majority) are permanently immutable.
In addition, if final
field mutation is not enabled on the command line, then sun.reflect.ReflectionFactory
is the only mechanism that can mutate final
fields. If the JVM knows that its deserialization methods will not mutate final
fields, then the JVM can treat the final
fields in deserialized objects as permanently immutable -- even for Serializable
classes. Fortunately, the JVM can do this for many JDK classes. For example, the classes that implement unmodifiable lists perform deserialization by invoking their constructors, so the deserialization methods of ReflectionFactory
will do the same and will not mutate their final
fields. As a result, the JVM can trust the final
fields of every unmodifiable list, regardless of which library deserialized the list.
Distinct from serialization libraries, frameworks for dependency injection, unit testing, and mocking use deep reflection to manipulate objects, including mutating final
fields. The maintainers of such frameworks should only ask users to enable final
field mutation on the command line as a last resort. Instead, maintainers should find architectural approaches that avoid the need to mutate final
fields (and access private
fields) altogether. For example, most dependency injection frameworks now forbid the injection of final
fields, and all discourage it, instead recommending constructor injection.
Mutating final
fields from native code
Native code can mutate Java fields by calling the Set<Type>Field
functions or the SetStatic<Type>Field
functions defined in the Java Native Interface (JNI).
The behavior of these functions on final
fields is undefined. This means that the function could mutate the field to the desired value, or mutate it to a different value, or not mutate it at all, or, e.g., mutate it correctly in 999 out of 1000 executions but cause a JVM crash in 1 out of 1000 executions. As we enhance the JVM's catalog of optimizations to exploit the final
field restrictions placed on Java code, the chance of oddball outcomes due to undefined behavior in native code becomes more likely.
There are already restrictions on executing native code due to the possibility of undefined behavior, so by default the JVM can assume that these functions are not called. However, if native access is enabled, then this JEP proposes new diagnostics to mitigate the risks of oddball outcomes from mutating final
fields via JNI:
-
If the application is started with unified logging enabled for native code (
-Xlog:jni=debug
), calling any of the functions mentioned above on afinal
field will cause a message to be logged:[0.20s][debug][jni] Set<Type>Field of final instance field C.f
or
[0.20s][debug][jni] SetStatic<Type>Field of final static field C.f
-
If the application is started with additional checking of JNI functions (
-Xcheck:jni
), calling any of the functions mentioned above on afinal
field will cause the JVM to terminate with an error message.
In a future JDK release, the functions mentioned above may be changed so that they always return successfully when called on final
fields, but never actually effect any mutation.
There are no diagnostics for when Java code mutates final
fields via the sun.misc.Unsafe
class. Such mutation may cause strange bugs or JVM crashes. Note that the process to remove the methods of sun.misc.Unsafe
that could be used to mutate final
fields has already begun in JDK 24.
Risks and Assumptions
-
The ability to mutate
final
fields has been part of the Java Platform since JDK 5, so there is a risk that existing applications will be impacted by thefinal
field restrictions. -
We assume that developers whose applications rely directly or indirectly on mutating
final
fields will be able to configure the Java runtime to enable the capability via--enable-final-field-mutation
. This is similar to how they can already configure the Java runtime to disable strong encapsulation for modules via--add-opens
.
Alternatives
-
Rather than enforce the immutability of
final
fields, the Java runtime could rely on speculation and optimistically assume thatfinal
fields are not mutated, detecting when they are, and deoptimizing code when that happens. While speculative optimizations are the bread-and-butter of the JVM's JIT compiler, they may not suffice in this case as future planned optimizations may wish to rely not only on immutability within the lifetime of the process, but also on the immutability of fields from one run of the application to the next. -
Instead of specifying the modules whose code can mutate
final
fields, we could specify the modules that allow their classes'final
fields to be mutated. However, since the mutation offinal
fields is generally undesirable, it is better for command-line options to record which modules should be changed to no longer perform mutation. Specifying the modules whosefinal
fields can be mutated would make it hard to know for what purpose they allow their fields to be mutated, and by whom. -
Requiring
--enable-final-field-mutation
to specify both sides — the module performing the mutation and the module containing the mutated field — is unnecessarily burdensome. In many practical cases,--enable-final-field-mutation
will be specified in conjunction with--add-opens
, which already specifies both sides of the reflective access.