JEP draft: Prepare to Make Final Mean Final

AuthorRon Pressler & Alex Buckley
OwnerRon Pressler
TypeFeature
ScopeSE
StatusSubmitted
Componentcore-libs
Discussionjdk dash dev at openjdk dot org
Reviewed byAlan Bateman, Brian Goetz
Created2025/02/06 10:25
Updated2025/08/27 15:57
Issue8349536

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

Non-Goals

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:

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:

The effect of these API changes is:

The behaviors of related methods also change:

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:

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:

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

Alternatives