Quality Outreach Heads-up - JDK 26: Warnings About Final Field Mutation

The OpenJDK Quality Group is promoting the testing of FOSS projects with OpenJDK builds as a way to improve the overall quality of the release. This heads-up is part of a Quality Outreach update sent to the projects involved. To learn more about the program and how to join, please check the Quality Outreach wiki page.

Reflective Mutation of Final Fields

Java’s reflection API is a powerful meta-programming tool and a cornerstone of the ecosystem’s capabilities - without it, many important frameworks and libraries were impossible. It does not come without downsides, though. One is its ability to undermine the integrity of the keyword final, which promises that such a field is assigned exactly once, but, with reflection, that promise can be easily broken:

void main() throws Exception {
	var box = new Box("element");
	// prints "[element]"
	IO.println(box);

	var elementField = Box.class.getDeclaredField("element");
	elementField.setAccessible(true);
	elementField.set(box, "new element");

	// prints "[new element]"
	IO.println(box);
}

class Box {

	final String element;

	Box(String element) {
		this.element = element;
	}

	public String toString() {
		return "[" + element + "]";
	}

}

Final field mutation has unfortunate downstream effects where developers can’t rely on established invariants for correctness or for security, and the just-in-time compiler can’t apply all possible optimizations (particularly constant-folding). This is why recent additions to Java that deal with fields do not allow their mutation through reflection: hidden classes, records, and the preview API LazyConstant.

Integrity by Default

The characteristics of reflection follow a pattern where Java occasionally makes guarantees whose integrity can be undermined with the very tools it comes with. Of course, the application of these tools can be generally beneficial (as is the case here) but that doesn’t erase the negative impact on a project’s maintainability, security, or performance that comes from undermining Java’s integrity. There is clearly a tradeoff.

Since the additional capabilities as well as the potential downsides are ultimately borne by applications (not frameworks nor libraries), it should be their developers or operators who make the tradeoff. This is achieved by making Java stricter by default, by making it uphold the integrity of its guarantees unless specific actions are taken by application maintainers. So the various ways to undermine Java’s integrity are either replaced by features without that capability or (more often) guarded by command-line options:

  • module internals are strongly encapsulated by default, but access is possible with --add-exports and --add-opens
  • attaching an agent at run time leads to a warning (in the future, an error) unless allowed with -XX:+EnableDynamicAgentLoading
  • calling a restricted JNI or FFM method causes a warning (in the future, an error) unless allowed with --enable-native-access
  • Unsafe’s memory access methods are being removed after having been replaced by the foreign memory API

As proposed by JEP 500 and starting with JDK 26, the same is true for reflective mutation of final fields.

New Warnings and (Soon) Errors

If no new command-line options are applied, mutating a final field through the reflection API java.lang.reflect.Field::set on JDK 26 leads to a warning like the following on the standard error stream:

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

This warning is emitted once per reflecting module. Consequently, if the entire application resides on the class path only one such warning is printed.

In future Java releases, this warning will become an error, at which point command-line options are required to mutate final fields through reflection.

Adapting to Immutable Final Fields

Two new command-line options are introduced that application maintainers can use to handle these warnings/errors:

  • --enable-final-field-mutation is a permanent option that allows specific modules to mutate final fields
  • --illegal-final-field-mutation is a temporary option with values allow, warn (default on JDK 26), debug, and deny (future default) that manages how code without specific permission that tries to mutate final fields will be handled

JEP 500 as well as Inside Java Newscast #101 discuss the use of these options as well as their interaction with strong encapsulation in detail, which is particularly important for application maintainers.

However, even given theses options, as Java moves towards a future where final fields are truly immutable by default, it is imperative for library and framework maintainers (and to a lesser degree, even application developers) to avoid mutating them through the reflection API. The article Avoiding Final Field Mutation examines various scenarios in which final fields are commonly mutated via reflection and discusses alternative approaches for each.

~