JEP draft: Prepare to Make Final Mean Final
Author | Ron Pressler & Alex Buckley |
Owner | Ron Pressler |
Type | Feature |
Scope | SE |
Status | Submitted |
Component | core-libs |
Discussion | jdk dash dev at openjdk dot org |
Reviewed by | Alan Bateman, Brian Goetz |
Created | 2025/02/06 10:25 |
Updated | 2025/08/27 15:57 |
Issue | 8349536 |
Summary
Issue warnings about uses of deep reflection, i.e., reflection that overrides access controls, to mutate final fields. These warnings aim to prepare developers for a future release that ensures integrity by default by restricting final field mutation, which will make 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 classes with final 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
Final fields 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 reasoning about correctness.
The expectation that a final field cannot be reassigned is also important for performance. The more constraints there are on the behavior of a class, the more optimizations the JVM 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 evaluates a constant expression just once rather than every time it is used. Constant folding is often the first step in a chain of optimizations that together can provide significant speedups.
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 a program, undermining all reasoning about correctness and invalidating important optimizations. The most prevalent of these APIs is deep reflection, as embodied in the setAccessible
and set
methods of the java.lang.reflect.Field
class. These methods allow you to mutate final fields at will, overriding the Java language’s access controls. For example:
// A normal class with a final field
class C {
final int x;
C() { x = 100; }
}
// 1. Perform deep reflection over the final field in C
java.lang.reflect.Field f = C.class.getDeclaredField("x");
f.setAccessible(true); // Make C's final field mutable
// 2. Create an instance of 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
Final fields are, in reality, as mutable as a non-final fields. We cannot use final fields to construct the deeply immutable graphs of objects that enable the JVM to deliver the best performance optimizations.
It might seem absurd that the Java Platform provides an API that undermines the meaning of the final
keyword. After JDK 5 introduced the Java Memory Model, which 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 the 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 the process to remove methods in sun.misc.Unsafe
that, like deep reflection, allow the mutation of final fields.
Relatively little code mutates final fields, but the mere existence of APIs for doing so makes it impossible 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 propose to enforce the immutability of final fields so that, by default, code cannot use deep reflection to reassign them at will. We will support one special use case, namely 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, i.e., the setAccessible
and set
methods of java.lang.reflect.Field
. In JDK XX, we will restrict deep reflection so that, by default, mutating a final field causes a warning to be issued at run time. It will not be possible to avoid the warning simply by using --add-opens
to enable the 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, by default, throw exceptions when Java code uses deep reflection to mutate final fields. This will enable the Java Platform and applications running on it to have integrity by default. The warning we introduce here is intended to prepare 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 alternatives. Enabling final field mutation acknowledges the application's need to mutate final fields and lifts selected 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 this 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 below.
Most application developers who wish to allow final field mutation will pass the --enable-final-field-mutation
option 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-Final-Field-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-Final-Field-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 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.
The --enable-final-field-mutation
option can refer to modules in the boot module layer only. It is not possible to enable final field mutation for code in user-defined layers.
API changes
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 specification of of Field::set
is revised as follows (changes in bold):
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 the
Field
object; and- final field mutation is enabled for the caller's module; and
- the field's declaring class is in a package that is open to the caller's module; and
- the field's declaring class is not a record class; and
- the field's declaring class is not a hidden class; and
- the field is non-static.
If this method is called from native code, then there is no caller class on the stack and it is not possible to determine the caller's module. In this case, the Field object has write access to an underlying final field if and only the following conditions are met:
- setAccessible(true) has succeeded for the
Field
object; and- final field mutation is enabled for the unnamed module; and
- the field is declared public, in a class that is public, in a package that is exported to all modules; and
- the field's declaring class is not a record class; and
- the field's declaring class is not a hidden class; and
- the field is non-static.
If any of the above checks is not met, this method throws an
IllegalAccessException
.Note: 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 public in an exported package. The field must be declared in a package that is open.Note: 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
's module declaration containsopens p
oropens p to N
, orM
is an automatic module, i.e., it is placed on the module path but it has no module declaration, 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.
The effect of these API changes is:
-
If final field mutation is not enabled for a module then it is illegal for code in that module to mutate any final field via deep reflection, even if the final field is in that module.
That is, given a
Field
objectf
that reflects a final field, it may be legal for code in the module to callf.setAccessible(true)
but illegal for it to callf.set(..., ...)
. -
If final field mutation is enabled for a module, but there is a final field in some package which is not open to the module, then it is illegal for code in that 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 passesf
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 receivesf
to callf.set(..., ...)
.
The behaviors of related methods also change:
-
The behavior of
MethodHandles.Lookup::unreflectSetter
is changed in the same way 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 mutate final fields in the package, even if final field mutation was enabled forO
on the command line. This is because the JVM already decided to trust the final fields in the package based on it not being open toO
at startup.The same applies to
ModuleLayer.Controller::addOpens
andInstrumentation::redefineModule
. -
The
setIn
,setOut
, andsetErr
methods of thejava.lang.System
class exist to mutate, respectively, the final fieldsin
,out
, anderr
of that class. These fields have always been write-protected, which means they can be mutated only by calling the corresponding methods. 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
What action the Java runtime takes when an illegal final field mutation is attempted is controlled by a new command-line option, --illegal-final-field-mutation
. This option 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 illegal final 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=debug
is identical towarn
except both a warning message and a stack trace are issued for every illegal final field mutation. -
--illegal-final-field-mutation=deny
will result inField::set
throwing anIllegalAccessException
for every illegal final field mutation.This mode will become the default in a future release.
When deny
becomes the default mode, allow
will be removed but warn
and debug
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 the 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-final-field-mutation=N to avoid a warning
WARNING: Mutating final fields will be blocked in a future release unless final field mutation is enabled
By default, 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.
Serialization libraries should use sun.reflect.ReflectionFactory
The ability to mutate final fields via deep reflection was added in JDK 5 so that third-party serialization libraries could provide functionality on par with the JDK's own serialization facilities. The JDK can deserialize an object from an input stream even if the object's class declares final fields. It does this by bypassing the class's constructors, which ordinarily assign instance fields, and assigning 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 ask users to enable final field mutation on the command line, maintainers of serialization libraries should serialize and deserialize objects using the sun.reflect.ReflectionFactory
API, which is supported for this purpose. This API allows a serialization library to obtain a method handle to special code that initializes an object by assigning to its instance fields directly, including final fields. This code, which is dynamically generated by the JDK, gives the serialization library the same powers as the JDK's own serialization facilities; it is not necessary to enable final field mutation by the module of the serialization library.
The sun.reflect.ReflectionFactory
class only supports the deserialization of objects whose classes implement the java.io.Serializable
interface. 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 it can also assume that final fields in all other objects — which are the vast majority — are permanently immutable.
If final field mutation is not enabled then sun.reflect.ReflectionFactory
is the only mechanism that can mutate final fields. If the JVM detects that the method handle returned by the ReflectionFactory
API for a particular class will not mutate final fields then it can treat the final fields in that class as permanently immutable. Fortunately, the JVM may be able do this for many JDK classes. For example, the JDK classes that implement unmodifiable lists deserialize by invoking their constructors rather than by assigning to their instance fields. For these classes, the ReflectionFactory
code could delegate to the classes' deserialization methods and thus avoid mutating final fields. Knowing this, the JVM could trust the final fields of every unmodifiable list, even for lists deserialized by a third-party library.
Libraries should not use deep reflection to mutate final fields
Some libraries and frameworks for dependency injection, unit testing, and mocking use deep reflection to manipulate objects, including by mutating final fields. The maintainers of such components should only ask users to enable final field mutation as a last resort. Instead, maintainers should find architectural approaches that avoid the need to mutate final fields or 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.
Cloning should not use deep reflection
Authors of classes with final fields have long faced challenges implementing the clone
method. If an implementation of clone
calls super.clone()
then it cannot customize the values of final fields in the returned object simply by assigning to them. Implementations of clone
sometimes use deep reflection to mutate these fields, but that will not work in a future JDK release that disallows the mutation of final fields by default.
Joshua Bloch's 2001 book Effective Java recommends avoiding clone
and declaring static factory methods instead (Item 11: "Override clone()
judiciously"). In a class that must continue to implement clone
, super.clone()
should be replaced with code that instantiates the class via a (possibly non-public) constructor. Because the constructor can initialize final fields to the desired values, the clone
method need not use deep reflection.
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 outcome of calling these functions on final fields is undefined behavior. This means that Java constructs from which the program is built, such as objects, arrays, and types, no longer have integrity. The JVM can no longer guarantee that their behavior conforms to their specifications; for example, the program could access an array beyond its bound without the JVM raising an exception, resulting in memory corruption or a process crash. As we enhance the JVM's catalog of optimizations that 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 we propose new diagnostics to mitigate the risks of oddball outcomes due to mutating final fields via JNI:
-
If the application is started with unified logging enabled for native code (
-Xlog:jni=debug
), calling any of the JNI functions mentioned above on a final 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 enabled (
-Xcheck:jni
), calling any of the JNI functions mentioned above on a final field will cause the JVM to terminate with an error message.
In a future JDK release, we may change the JNI functions mentioned above so that they always return successfully when called on final fields, but never actually do any mutation.
There are no diagnostics for when Java code mutates final fields via the sun.misc.Unsafe
API. Such mutations may violate integrity, and may cause strange bugs or JVM crashes. The process to remove the sun.misc.Unsafe
methods that can be used to mutate final fields was begun in JDK 24.
Risks and Assumptions
-
The ability to mutate final fields has been part of the Java Platform since JDK 5. There is a risk that existing applications will be impacted by the proposed final field restrictions.
-
We assume that developers whose applications rely directly or indirectly on final field mutation 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: It could optimistically assume that final fields are not mutated, detect when they are, and then deoptimize code as necessary when that happens. While speculative optimizations are the bread-and-butter of the JVM's JIT compiler, they may not suffice in this case. Future planned optimizations may 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 ask developers to specify the modules that allow their final fields to be mutated. However, since the mutation of final fields is undesirable, it is best to record on the command line the modules whose code should be updated to no longer attempt mutation. Specifying instead the modules whose final fields can be mutated would not record why they allow their fields to be mutated, nor encourage libraries to migrate away from mutating those fields.
-
We could require
--enable-final-field-mutation
to specify both the module performing the mutation and the modules containing the mutated fields. This would be unnecessarily burdensome. In many practical cases,--enable-final-field-mutation
will be specified in conjunction with--add-opens
, which already specifies both parties to the use of deep reflection.