JEP draft: Prepare to Make Final Mean Final

AuthorRon Pressler & Alex Buckley
OwnerRon Pressler
TypeFeature
ScopeJDK
StatusDraft
Componentcore-libs
Discussionjdk dash dev at openjdk dot org
Created2025/02/06 10:25
Updated2025/04/02 06:40
Issue8349536

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

Non-Goals

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:

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:

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:

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:

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:

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

Alternatives