Preparing for final Field Mutation Restrictions

In the Java programming language, fields declared as final cannot be mutated, that is, assigned after initialization. However, some Java SE APIs can enable the mutation of final fields at run time. In particular, the AccessibleObject.setAccessible(boolean) method in the java.lang.reflect package enables deep reflection, which is reflection over fields and methods without regard to encapsulation boundaries. Any code can use it to invoke the private methods of any class, read and write the private fields of any object, and even write to final fields.

Starting in JDK 26, the JDK generates a warning similar to the following for the first mutation of a final field through deep reflection.

WARNING: Final field configValue in class com.example.InternalService
has been mutated reflectively by class com.example.bean.BeanPropertyField
in unnamed module @c387f44 (file:/usr/lib/library-utils.jar)
WARNING: Use --enable-final-field-mutation=ALL-UNNAMED to avoid a warning
WARNING: Mutating final fields will be blocked in a future release unless
final field mutation is enabled

The warning message indicates the field that has been mutated and the code that mutated the field.

In a future release, the JDK will instead throw an exception. Consequently, to prepare for these restrictions, you can selectively enable the ability to mutate final fields through deep reflection where essential.

See JEP 500: Prepare to Make Final Mean Final.

The --enable-final-field-mutation Command-Line Option

Starting in JDK 26, a warning is generated for the first mutation of a final field through deep reflection.

You can suppress these warnings by specifying the --enable-final-field-mutation command-line option, which enables code to mutate final instance fields through deep reflection when the field's declaring class is in a package that's open to the module mutating the field.

To enable code on the class path to mutate final fields through deep reflection, specify the value ALL-UNNAMED for this option. The following command suppresses the final field mutation warnings:

java --enable-final-field-mutation=ALL-UNNAMED Application

To enable code on the module path to mutate final fields through deep reflection, specify the modules in a comma-separated list, for example:

java --enable-final-field-mutation=MyModule1,MyModule2,MyModule3 MyModularApp

The --illegal-final-field-mutation Command-Line Option

Alternatively, you can specify the command-line option --illegal-final-field-mutation, which affects all code in the system and not just code on the code path or in specific modules. In addition, this option handles final field mutations differently depending on the value assigned to it:

  • --illegal-final-field-mutation=allow allows final field mutations through deep reflection to proceed without warning.

  • --illegal-final-field-mutation=warn allows final field mutation through deep reflection but issues a warning the first time that code in a particular module performs such a mutation. At most one warning per module is issued.

    Starting in JDK 26, this is the default mode. This mode will be phased out and removed in a future release.

  • --illegal-final-field-mutation=debug is identical to warn except both a warning message and a stack trace are issued for every final field mutation through deep reflection.

  • --illegal-final-field-mutation=deny results in Field::set throwing an IllegalAccessException for every final field mutation through deep reflection.

    This mode will become the default in a future release.

Tip:

Test your code with --illegal-final-field-mutation=deny to ensure that it continues to work after the JDK denies final field mutation by default in a future release.

Which Option to Use, --illegal-final-field-mutation or --enable-final-field-mutation?

Use the --illegal-final-field-mutation option to help you identify which libraries mutate final fields through deep reflection so that you can ideally update the libraries to newer versions that don't mutate final fields.

Use the --enable-final-field-mutation option if you use libraries that mutate final fields through deep reflection, and you are unable to remove, replace, or modify these libraries.

Note:

  • It's recommended that you use neither --illegal-final-field-mutation nor --enable-final-field-mutation. Using either option means your application still depends on libraries that mutate final fields through deep reflection. This is potentially insecure, so it's preferable to remove, replace, or modify the libraries identified by JDK warnings.
  • It's recommended that you use --illegal-final-field-mutation=allow only as a temporary measure while you decide how to handle final field mutation in your application.

Identifying Code That Mutates final Fields Through Deep Reflection

Code that mutates final fields through deep reflection is usually library code, not application code. You can identify precisely which code mutates final fields through deep reflection by starting the Java runtime with the command-line option --illegal-final-field-mutation=debug or with JDK Flight Recorder (JFR) enabled.

The following example runs a Java application with the --illegal-final-field-mutation=debug command-line option. A warning and a stack trace are issued for every final field mutation through deep reflection.

$ java --illegal-final-field-mutation=debug Application
...
Final field maxCapacity in class com.example.ResourceLibrary$Storage has been mutated
reflectively by class com.example.ResourceLibrary in unnamed module @3d4eac69
(file:/usr/lib/UtilLibrary.jar)
        at java.base/java.lang.reflect.Field.postSetFinal(Field.java:1517)
        at java.base/java.lang.reflect.Field.setFinal(Field.java:1431)
        at java.base/java.lang.reflect.Field.set(Field.java:891)
        at com.example.ResourceLibrary.addUnit(ResourceLibrary.java:24)
        at Application.main(Application.java:34)

In this example, a method in com.example.ResourceLibrary mutates a final field. When JFR is enabled, the JVM records a jdk.FinalFieldMutation event whenever code mutates a final instance field through deep reflection or uses Lookup::unreflectSetter to get a MethodHandle with write access to a reflected final field. This event identifies the class declaring the final field and the name of the final field. The event also generates a stack trace to show where the final field mutation is coming from.

The following example runs a Java application with JFR enabled and then displays jdk.FinalFieldMutation events in the JFR recording:

$ java -XX:StartFlightRecording:filename=recording.jfr --enable-final-field-mutation=ALL-UNNAMED Application
[0.917s][info][jfr,startup] Started recording 1. No limit specified, using maxsize=250MB as default.
[0.917s][info][jfr,startup]
[0.917s][info][jfr,startup] Use jcmd 5102 JFR.dump name=1 to copy recording data to file.
...
$ jfr print --events jdk.FinalFieldMutation recording.jfr
jdk.FinalFieldMutation {
  startTime = 23:08:26.089 (2026-01-14)
  declaringClass = com.example.ResourceLibrary$Storage (classLoader = app)
  fieldName = "maxCapacity"
  eventThread = "main" (javaThreadId = 3)
  stackTrace = [
    java.lang.reflect.Field.set(Object, Object) line: 891
    com.example.ResourceLibrary.addUnit() line: 24
    Application.main(String[]) line: 34
  ]
}