finalフィールドの変更制限への準備

Javaプログラミング言語では、finalとして宣言されたフィールドは変更できません。つまり、初期化後に値を代入することはできません。ただし、一部のJava SE APIでは、実行時にfinalフィールドを変更することが可能です。特に、java.lang.reflectパッケージのAccessibleObject.setAccessible(boolean)メソッドは、カプセル化の境界に関係なくフィールドとメソッドをリフレクションするディープ・リフレクションを可能にします。これを使用すると、任意のコードから任意のクラスのprivateメソッドを呼び出したり、任意のオブジェクトのprivateフィールドを読み書きしたり、finalフィールドへの書き込みも行うことができます。

JDK 26以降、JDKは、ディープ・リフレクションによるfinalフィールドの最初の変更に対して、次のような警告を出力します。

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

警告メッセージには、変更されたフィールドと、そのフィールドを変更したコードが示されます。

将来のリリースでは、JDKはかわりに例外をスローします。したがって、これらの制限に備えるために、必要な場合にかぎり、ディープ・リフレクションによってfinalフィールドを変更する機能を選択的に有効にできます。

JEP 500: finalを最終にする準備を参照してください。

--enable-final-field-mutationコマンドライン・オプション

JDK 26以降、ディープ・リフレクションによるfinalフィールドの最初の変更に対して警告が出力されます。

これらの警告を抑制するには、--enable-final-field-mutationコマンドライン・オプションを指定します。これにより、フィールドを宣言しているクラスが、フィールドを変更するモジュールに対して開かれているパッケージ内にある場合に、コードがディープ・リフレクションによってfinalインスタンス・フィールドを変更できるようになります。

クラス・パス上のコードがディープ・リフレクションを介してfinalフィールドを変更できるようにするには、このオプションに値ALL-UNNAMEDを指定します。次のコマンドは、finalフィールドの変更の警告を抑制します:

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

モジュール・パス上のコードがディープ・リフレクションを介してfinalフィールドを変更できるようにするには、モジュールをカンマ区切りのリストで指定します。次に例を示します:

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

--illegal-final-field-mutationコマンドライン・オプション

または、コマンドライン・オプション--illegal-final-field-mutationを指定することもできます。これは、コード・パス上または特定のモジュール内のコードのみでなく、システム内のすべてのコードに影響します。また、このオプションでは、割り当てられた値に応じてfinalフィールドの変更が異なって処理されます:

  • --illegal-final-field-mutation=allowでは、ディープ・リフレクションによるfinalフィールドの変更を警告なしで実行できます。

  • --illegal-final-field-mutation=warnでは、ディープ・リフレクションによるfinalフィールドの変更は許可されますが、特定のモジュール内のコードがそのような変更を最初に実行したときに警告が出力されます。モジュールごとに最大1つの警告が発行されます。

    JDK 26以降は、これがデフォルト・モードです。このモードは、将来のリリースで廃止および削除されます。

  • --illegal-final-field-mutation=debugwarnと同じですが、ディープ・リフレクションによるすべてのfinalフィールドの変更に対して警告メッセージとスタック・トレースの両方が出力されます。

  • --illegal-final-field-mutation=denyでは、ディープ・リフレクションによるすべてのfinalフィールドの変更に対してField::setIllegalAccessExceptionをスローします。

    このモードは、将来のリリースでデフォルトとなる予定です。

ヒント:

--illegal-final-field-mutation=denyを使用してコードをテストし、JDKが将来のリリースでデフォルトでfinalフィールドの変更を拒否するようになった後も引き続き動作することを確認します。

--illegal-final-field-mutationと--enable-final-field-mutationのどちらのオプションを使用すべきか

--illegal-final-field-mutationオプションを使用すると、ディープ・リフレクションによってfinalフィールドを変更するライブラリを特定できます。これにより、理想的には、finalフィールドを変更しない新しいバージョンのライブラリに更新することが可能になります。

ディープ・リフレクションによってfinalフィールドを変更するライブラリを使用しており、これらのライブラリを削除、置換または変更できない場合は、--enable-final-field-mutationオプションを使用します。

ノート:

  • --illegal-final-field-mutation--enable-final-field-mutationも使用しないことをお薦めします。いずれかのオプションを使用すると、アプリケーションは、ディープ・リフレクションによってfinalフィールドを変更するライブラリに依存したままになります。これは安全でない可能性があるため、JDKの警告によって識別されるライブラリを削除、置換または変更することをお薦めします。
  • --illegal-final-field-mutation=allowは、アプリケーションでfinalフィールドの変更を処理する方法を決定するまでの間、一時的な措置としてのみ使用することをお薦めします。

ディープ・リフレクションによってfinalフィールドを変更するコードの識別

finalフィールドをディープ・リフレクションによって変更するコードは、通常、アプリケーション・コードではなくライブラリ・コードです。コマンドライン・オプション--illegal-final-field-mutation=debugまたはJDK Flight Recorder (JFR)を有効にしてJavaランタイムを起動することにより、ディープ・リフレクションによってfinalフィールドを変更するコードを正確に識別できます。

次の例では、--illegal-final-field-mutation=debugコマンドライン・オプションを使用してJavaアプリケーションを実行します。ディープ・リフレクションによるすべてのfinalフィールドの変更に対して警告およびスタック・トレースが出力されます。

$ 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)

この例では、com.example.ResourceLibraryのメソッドがfinalフィールドを変更します。JFRが有効な場合、コードがディープ・リフレクションを介してfinalインスタンス・フィールドを変更するか、Lookup::unreflectSetterを使用して、リフレクションされたfinalフィールドへの書込みアクセス権を持つMethodHandleを取得するたびに、JVMはjdk.FinalFieldMutationイベントを記録します。このイベントは、finalフィールドを宣言するクラスと、finalフィールドの名前を識別します。また、このイベントは、finalフィールドの変更の発生元を示すスタック・トレースも生成します。

次の例では、JFRを有効にしてJavaアプリケーションを実行し、JFR記録にjdk.FinalFieldMutationイベントを表示します:

$ 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
  ]
}