JEP 500: Prepare to Make Final Mean Final
Подготовка к тому, чтобы final действительно означало final
| Автор | Ron Pressler & Alex Buckley |
| Ответственный | Ron Pressler |
| Тип | Feature |
| Область | SE |
| Статус | Closed / Delivered |
| Выпуск | 26 |
| Компонент | core-libs |
| Обсуждение | jdk dash dev at openjdk dot org |
| Рецензенты | Alan Bateman, Brian Goetz |
| Одобрен | Mark Reinhold |
| Создан | 2025/02/06 10:25 |
| Обновлён | 2026/01/21 18:07 |
| Задача | 8349536 |
Аннотация
Выдавать предупреждения при использовании глубокой рефлексии для изменения final-полей. Эти предупреждения призваны подготовить разработчиков к будущему выпуску, который обеспечит целостность по умолчанию, ограничив изменение final-полей, что сделает программы на Java безопаснее и, возможно, быстрее. Разработчики приложений могут избежать и нынешних предупреждений, и будущих ограничений, выборочно разрешив изменение final-полей там, где это необходимо.
Цели
-
Подготовить экосистему Java к будущему выпуску, который по умолчанию запретит изменение final-полей с помощью глубокой рефлексии. Начиная с этого выпуска, разработчикам приложений придётся явно включать такую возможность при запуске.
-
Привести final-поля в обычных классах в соответствие с неявно объявленными полями классов Records (записи), которые нельзя изменить с помощью глубокой рефлексии.
-
Позволить библиотекам сериализации продолжать работать с классами
Serializable, даже с классами, у которых есть final-поля.
Что не является целью
-
Целью не является объявление устаревшей или удаление какой-либо части API платформы Java.
-
Целью не является запрет на изменение final-полей библиотеками сериализации во время десериализации.
Мотивация
Final-поля представляют неизменяемое состояние. После того как final-полю присвоено значение в конструкторе (для final-полей экземпляра) или в инициализаторе класса (для статических final-полей), его нельзя присвоить повторно. Его значение, будь то примитивное значение или ссылка на объект, неизменяемо. Ожидание, что final-поле нельзя переприсвоить в отдалённых частях программы, намеренно или случайно, часто имеет решающее значение при рассуждениях о корректности.
Ожидание, что final-поле нельзя переприсвоить, важно и для производительности. Чем больше ограничений на поведение класса, тем больше оптимизаций может применить JVM. Например, возможность полагаться на то, что final-поля никогда не переприсваиваются, позволяет JVM выполнять свёртку констант — оптимизацию, которая вычисляет константное выражение только один раз, а не при каждом его использовании. Свёртка констант часто бывает первым шагом в цепочке оптимизаций, которые вместе могут дать значительное ускорение.
К сожалению, ожидание, что final-поле нельзя переприсвоить, ложно. Платформа Java включает несколько API, которые позволяют любому коду программы в любой момент переприсваивать final-поля, что подрывает любые рассуждения о корректности и делает невозможными важные оптимизации. Самый распространённый из них — API глубокой рефлексии, воплощённый в методах setAccessible и set класса java.lang.reflect.Field. Эти методы позволяют изменять final-поля как угодно. Например:
// 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-поля так же изменяемы, как и поля без final. Мы не можем полагаться на неизменяемость final-полей при рассуждениях о корректности и не можем использовать final-поля для построения глубоко неизменяемых графов объектов, которые позволяют JVM применять самые эффективные оптимизации производительности.
Может показаться абсурдным, что платформа Java включает API, подрывающий смысл ключевого слова final. Final-поля играют ключевую роль в Java Memory Model начиная с JDK 5; в частности, на их неизменяемости основана безопасная инициализация объектов в многопоточном коде. К сожалению, их неизменяемость также противоречит работе библиотек сериализации, которые изменяют поля, чтобы инициализировать объекты во время десериализации. Этот сценарий использования был достаточно важным, чтобы оправдать изменение API рефлексии в JDK 5, после которого с его помощью стало можно изменять final-поля.
Оглядываясь назад, предоставление такой неограниченной функциональности было неудачным решением, потому что оно принесло в жертву целостность. Когда мы добавили Hidden Classes в JDK 15 и классы Records в JDK 16, мы ограничили глубокую рефлексию так, чтобы запретить изменение final-полей в hidden-классах и record-классах. Мы ещё сильнее ограничили глубокую рефлексию, когда строго инкапсулировали внутренние компоненты JDK в JDK 17. В JDK 24 мы начали процесс удаления методов в sun.misc.Unsafe, которые, как и глубокая рефлексия, позволяют изменять final-поля.
Кода, изменяющего final-поля, относительно немного, но само существование API, позволяющих это делать, не даёт доверять значению ни одного final-поля. Это ухудшает безопасность и производительность всех программ. В соответствии с политикой целостности по умолчанию мы предлагаем обеспечить неизменяемость final-полей, чтобы по умолчанию код не мог с помощью глубокой рефлексии переприсваивать их как угодно. Один особый сценарий использования, а именно библиотеки сериализации, которым нужно изменять final-поля во время десериализации, мы будем поддерживать через API ограниченного назначения.
Описание
В JDK 5 и более поздних выпусках final-поля можно изменять с помощью глубокой рефлексии, то есть методов setAccessible и set класса java.lang.reflect.Field. В JDK 26 мы ограничим глубокую рефлексию так, что по умолчанию изменение final-поля будет приводить к выдаче предупреждения во время выполнения. Избежать предупреждения, просто используя --add-opens, чтобы разрешить глубокую рефлексию классов с final-полями, будет нельзя.
Ограничения на изменение final-полей мы называем ограничениями final-полей. Со временем мы будем усиливать действие ограничений final-полей. Вместо выдачи предупреждений будущий выпуск JDK по умолчанию будет выбрасывать исключения, когда код на Java использует глубокую рефлексию для изменения final-полей. Это позволит платформе Java и работающим на ней приложениям обеспечить целостность по умолчанию. Предупреждение, которое мы вводим здесь, призвано подготовить разработчиков к этому будущему.
Разрешение изменения final-полей
Разработчики приложений могут избежать предупреждений, а в будущем и исключений, разрешив изменение final-полей для выбранного кода на Java в командной строке или другими способами. Разрешение изменения final-полей подтверждает, что приложению нужно изменять final-поля, и снимает выбранные ограничения final-полей.
Согласно политике целостности по умолчанию изменение final-полей разрешает разработчик приложения (или, возможно, тот, кто его развёртывает, по совету разработчика приложения), а не разработчики библиотек. Разработчикам библиотек, которые используют рефлексию для изменения final-полей, следует сообщить своим пользователям, что им нужно будет разрешить изменение final-полей одним из описанных ниже способов.
Чтобы разрешить изменение final-полей любым кодом на пути к классам, независимо от того, где объявлены final-поля, используйте такой параметр командной строки:
$ java --enable-final-field-mutation=ALL-UNNAMED ...
Чтобы разрешить изменение final-полей определёнными модулями на пути к модулям, также независимо от того, где объявлены final-поля, передайте список имён модулей через запятую:
$ java --enable-final-field-mutation=M1,M2 ...
Разрешение изменения final-полей для модуля не гарантирует, что код в модуле сможет выполнять глубокую рефлексию для изменения final-полей. Каждое изменяемое final-поле также должно быть открыто для кода, выполняющего глубокую рефлексию, как подробно описано ниже.
Большинство разработчиков приложений, желающих разрешить изменение final-полей, будут передавать параметр --enable-final-field-mutation напрямую программе запуска java в скрипте запуска, но доступны и другие способы:
-
Можно передать
--enable-final-field-mutationпрограмме запуска косвенно, задав переменную окруженияJDK_JAVA_OPTIONS. -
Можно поместить
--enable-final-field-mutationв файл аргументов, который передаётся программе запуска скриптом или конечным пользователем, напримерjava @config -
Можно добавить
Enable-Final-Field-Mutationв манифест исполняемого JAR-файла, то есть JAR-файла, который запускается черезjava -jar. (Единственное поддерживаемое значение записи манифестаEnable-Final-Field-Mutation—ALL-UNNAMED; другие значения приводят к выбросу исключения.) -
Если вы создаёте для своего приложения собственную среду выполнения Java, можно передать параметр
--enable-final-field-mutationвjlinkчерез параметр--add-options, чтобы изменение final-полей было разрешено в получившемся образе среды выполнения. -
JNI Invocation API позволяет нативному приложению встроить JVM в собственный процесс. Нативное приложение, использующее JNI Invocation API, может разрешить изменение final-полей для модулей во встроенной JVM, передав параметр
--enable-final-field-mutationпри создании JVM.
Параметр --enable-final-field-mutation может ссылаться только на модули в загрузочном слое модулей. Разрешить изменение final-полей для кода в пользовательских слоях нельзя.
Управление действием ограничений final-полей
Код не имеет права изменять final-поле с помощью глубокой рефлексии, если либо код находится в модуле, для которого изменение final-полей не разрешено, либо код находится в модуле, для которого пакет поля не открыт. Действие среды выполнения Java при попытке недопустимого изменения final-поля задаётся новым параметром командной строки --illegal-final-field-mutation. По духу и форме этот параметр похож на параметр --illegal-access, введённый в JEP 261 в JDK 9, и на --illegal-native-access, введённый в JEP 472 в JDK 24. Он работает так:
-
--illegal-final-field-mutation=allowразрешает изменение без предупреждения. -
--illegal-final-field-mutation=warnразрешает изменение, но выдаёт предупреждение, когда код в определённом модуле впервые выполняет недопустимое изменение final-поля. Для каждого модуля выдаётся не больше одного предупреждения.Этот режим используется по умолчанию в JDK 26. В будущем выпуске он будет постепенно выведен из употребления и в конце концов удалён.
-
--illegal-final-field-mutation=debugидентиченwarn, за исключением того, что при каждом недопустимом изменении final-поля выдаются и предупреждение, и трассировка стека. -
--illegal-final-field-mutation=denyприведёт к тому, чтоField::setбудет выбрасыватьIllegalAccessExceptionпри каждом недопустимом изменении final-поля.В будущем выпуске этот режим станет режимом по умолчанию.
Когда deny станет режимом по умолчанию, allow будет удалён, но warn и debug будут поддерживаться ещё как минимум один выпуск.
Чтобы подготовиться к будущему, мы рекомендуем запускать существующий код в режиме deny и выявлять код, который изменяет final-поля с помощью глубокой рефлексии.
Предупреждения об изменении final-полей
Когда код пытается выполнить недопустимое изменение final-поля, изменение будет выполнено, но среда выполнения Java по умолчанию выдаст предупреждение с указанием вызывающего кода:
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
По умолчанию для каждого модуля выдаётся не больше одного такого предупреждения и только в том случае, если для этого модуля предупреждение ещё не выдавалось. Предупреждение записывается в стандартный поток ошибок.
Выявление кода, изменяющего final-поля
Код, который изменяет final-поля с помощью глубокой рефлексии, обычно находится в библиотеках, а не в приложении. Точно выяснить, какой код изменяет final-поля, можно одним из способов:
-
запустить среду выполнения Java с
--illegal-final-field-mutation=debug, как описано выше, или -
запустить среду выполнения Java с включённым JDK Flight Recorder (JFR). Когда JFR включён, JVM записывает событие
jdk.FinalFieldMutationвсякий раз, когда код изменяет final-поле экземпляра или используетLookup.unreflectSetter, чтобы получитьMethodHandleс правом записи в отражённое final-поле. Это событие указывает класс, в котором объявлено final-поле, имя final-поля и трассировку стека, показывающую, откуда исходит изменение final-поля.Например, вот как создать запись JFR и затем вывести события
jdk.FinalFieldMutation:$ java -XX:StartFlightRecording:filename=recording.jfr ... $ jfr print --events jdk.FinalFieldMutation recording.jfr
API глубокой рефлексии
Поведение Field::setAccessible не меняется. Это значит, что когда код вызывает f.setAccessible(true) для объекта Field f, код должен либо находиться в том же модуле, что и поле, отражённое f, либо, если код находится в другом модуле, поле, отражённое f, должно быть доступно вызывающему коду через exports или opens. Если эти условия не выполнены, вызов выбрасывает InaccessibleObjectException.
Поведение Field::set в JDK 26 меняется. Если код вызывает f.set(...) для объекта Field f, а поле, отражённое f, является final, то поле изменяется, если:
f.setAccessible(true)уже успешно выполнен,- класс, в котором объявлено поле, находится в пакете, открытом для модуля вызывающего кода, и
- изменение final-полей разрешено для модуля вызывающего кода.
Условия 2 и 3 появились в JDK 26. Они приводят к следующему:
-
Если изменение final-полей не разрешено для модуля и код в этом модуле пытается изменить какое-либо final-поле с помощью глубокой рефлексии, выбрасывается
IllegalAccessException, если это не подавлено с помощью--illegal-final-field-mutation.То есть для объекта
Fieldf, который отражает final-поле, код в модуле может иметь право вызватьf.setAccessible(true), но не иметь права вызватьf.set(...). -
Если изменение final-полей разрешено для модуля и код в этом модуле пытается с помощью глубокой рефлексии изменить final-поле в каком-либо пакете, который не открыт для этого модуля, выбрасывается
IllegalAccessException, если это не подавлено с помощью--illegal-final-field-mutation.Такая ситуация возможна, когда код в одном модуле, для которого открыт пакет поля, вызывает
f.setAccessible(true), а затем передаётfкоду в другом модуле, для которого изменение final-полей разрешено, но пакет поля не открыт. Код, получившийf, не имеет права вызыватьf.set(...).
Меняется также поведение связанных методов:
-
Поведение
MethodHandles.Lookup::unreflectSetterменяется так же, как поведениеField::set. -
Метод
Module::addOpensпозволяет вызывающему коду в модулеMво время выполнения открыть пакет в модулеNдля другого модуляOпри условии, что пакет уже открыт дляM. Если ни дляM, ни дляNизменение final-полей не разрешено в командной строке, JVM будет доверять final-полям в этом пакете. После этого вызовaddOpensизMне позволитOизменять final-поля в пакете. Это так, даже если дляOизменение final-полей разрешено в командной строке.То же относится к
ModuleLayer.Controller::addOpensиInstrumentation::redefineModule. -
Методы
setIn,setOutиsetErrклассаjava.lang.Systemсуществуют для изменения соответственно final-полейin,outиerrэтого класса. Эти поля всегда были защищены от записи: их можно изменить только вызовом соответствующих методов. Изменить эти поля с помощью глубокой рефлексии никогда не было возможно. В JDK 26 эти поля и соответствующие им методы никак не меняются.
Подробное описание изменений API приведено в документе Compatibility & Specification Review.
Библиотекам сериализации следует использовать sun.reflect.ReflectionFactory
Возможность изменять final-поля с помощью глубокой рефлексии добавили в JDK 5, чтобы сторонние библиотеки сериализации могли предоставлять функциональность на уровне собственных средств сериализации JDK. JDK может десериализовать объект из входного потока, даже если класс объекта объявляет final-поля. Для этого он обходит конструкторы класса, которые обычно присваивают значения полям экземпляра, и присваивает значения из входного потока полям экземпляра напрямую — даже если они final. Сторонние библиотеки сериализации делают то же самое с помощью глубокой рефлексии.
Когда в одном из будущих выпусков JDK ограничения на final-поля будут ужесточены, библиотеки сериализации больше не смогут использовать глубокую рефлексию без дополнительной настройки. Вместо того чтобы просить пользователей разрешать изменение final-полей в командной строке, разработчикам библиотек сериализации следует сериализовать и десериализовать объекты с помощью API sun.reflect.ReflectionFactory, который поддерживается для этой цели. Этот API позволяет библиотеке сериализации получить method handle (дескриптор метода) на специальный код, который инициализирует объект, присваивая значения его полям экземпляра напрямую, включая final-поля. Этот код, динамически генерируемый JDK, даёт библиотеке сериализации те же возможности, что и собственные средства сериализации JDK; разрешать изменение final-полей для модуля библиотеки сериализации не нужно.
Класс sun.reflect.ReflectionFactory поддерживает десериализацию только тех объектов, классы которых реализуют интерфейс java.io.Serializable. Это ограничение уравновешивает интересы разработчиков, использующих библиотеки сериализации, с более широким интересом всех разработчиков в корректном и эффективном выполнении. Оно гарантирует, что JVM при выполнении оптимизаций, таких как свёртка констант, не будет чрезмерно ограничена в допущениях, которые может делать: она должна допускать, что final-поля в объектах Serializable потенциально изменяемы, но может также допускать, что final-поля во всех остальных объектах — а их подавляющее большинство — неизменяемы навсегда.
Если изменение final-полей не разрешено, то sun.reflect.ReflectionFactory — единственный механизм, который может изменять final-поля. Если JVM обнаружит, что method handle, возвращаемый API ReflectionFactory для определённого класса, не будет изменять final-поля, то она может считать final-поля этого класса неизменяемыми навсегда. К счастью, JVM, возможно, сможет делать это для многих классов JDK. Например, классы JDK, реализующие неизменяемые списки, при десериализации вызывают свои конструкторы, а не присваивают значения полям экземпляра. Для таких классов код ReflectionFactory мог бы делегировать работу методам десериализации этих классов и тем самым избежать изменения final-полей. Зная это, JVM могла бы доверять final-полям каждого неизменяемого списка, даже списков, десериализованных сторонней библиотекой.
Библиотекам не следует изменять final-поля с помощью глубокой рефлексии
Некоторые библиотеки и фреймворки для внедрения зависимостей, модульного тестирования и создания mock-объектов используют глубокую рефлексию для работы с объектами, в том числе для изменения final-полей. Разработчикам таких компонентов следует просить пользователей разрешить изменение final-полей только в крайнем случае. Вместо этого им следует найти архитектурные решения, при которых вообще не нужно изменять final-поля или обращаться к private-полям. Например, большинство фреймворков внедрения зависимостей теперь запрещают внедрение в final-поля, а все остальные не рекомендуют его и вместо этого рекомендуют внедрение через конструктор.
Клонированию не следует использовать глубокую рефлексию
Авторы классов с final-полями давно сталкиваются с трудностями при реализации метода clone. Если реализация clone вызывает super.clone(), то она не может задать нужные значения final-полей в возвращаемом объекте простым присваиванием. Реализации clone иногда изменяют эти поля с помощью глубокой рефлексии, но в будущем выпуске JDK, где изменение final-полей по умолчанию будет запрещено, это работать не будет.
В книге Joshua Bloch Effective Java 2001 года рекомендуется избегать clone и вместо этого объявлять статические фабричные методы (Item 11: «Override clone() judiciously»). В классе, который должен и дальше реализовывать clone, super.clone() следует заменить кодом, создающим экземпляр класса через конструктор (возможно, не public). Поскольку конструктор может инициализировать final-поля нужными значениями, методу clone не нужно использовать глубокую рефлексию.
Изменение final-полей из нативного кода
Нативный код может изменять поля Java, вызывая функции Set<type>Field или функции SetStatic<type>Field, определённые в Java Native Interface (JNI).
Результат вызова этих функций для final-полей — неопределённое поведение. Это значит, что конструкции Java, из которых построена программа, такие как объекты, массивы и типы, больше не сохраняют целостность. JVM больше не может гарантировать, что их поведение соответствует их спецификациям; например, программа может обратиться к массиву за его границами без исключения со стороны JVM, что приведёт к повреждению памяти или аварийному завершению процесса. По мере того как мы расширяем набор оптимизаций JVM, использующих ограничения на final-поля в коде Java, вероятность странных результатов из-за неопределённого поведения в нативном коде растёт.
Из-за возможности неопределённого поведения уже существуют ограничения на выполнение нативного кода, поэтому по умолчанию JVM может считать, что эти функции не вызываются. Однако если нативный доступ разрешён, мы предлагаем новые средства диагностики, чтобы снизить риск странных результатов из-за изменения final-полей через JNI:
-
Если приложение запущено с включённым для нативного кода унифицированным журналированием (
-Xlog:jni=debug), вызов любой из упомянутых выше функций JNI для final-поля приведёт к записи сообщения в журнал:[0.20s][debug][jni] Set<type>Field of final instance field C.fили
[0.20s][debug][jni] SetStatic<type>Field of final static field C.f -
Если приложение запущено с включённой дополнительной проверкой функций JNI (
-Xcheck:jni), вызов любой из упомянутых выше функций JNI для final-поля приведёт к выводу предупреждения.
В одном из будущих выпусков JDK мы можем изменить упомянутые выше функции JNI так, чтобы при вызове для final-полей они всегда успешно завершались, но никогда на самом деле ничего не изменяли.
Средств диагностики для случаев, когда код Java изменяет final-поля через API sun.misc.Unsafe, нет. Такие изменения могут нарушать целостность и приводить к странным ошибкам или аварийному завершению JVM. Процесс удаления методов sun.misc.Unsafe, с помощью которых можно изменять final-поля, начался в JDK 24.
Риски и допущения
-
Возможность изменять final-поля является частью платформы Java начиная с JDK 5. Существует риск, что предлагаемые ограничения на final-поля затронут существующие приложения.
-
Мы предполагаем, что разработчики, приложения которых прямо или косвенно зависят от изменения final-полей, смогут настроить среду выполнения Java так, чтобы разрешить эту возможность с помощью
--enable-final-field-mutation. Это похоже на то, как они уже могут настроить среду выполнения Java, чтобы отключить строгую инкапсуляцию модулей с помощью--add-opens.
Альтернативы
-
Вместо того чтобы обеспечивать неизменяемость final-полей, среда выполнения Java могла бы полагаться на спекуляцию: оптимистично считать, что final-поля не изменяются, обнаруживать, когда они всё же изменяются, и в этом случае при необходимости деоптимизировать код.
Хотя спекулятивные оптимизации — основа работы JIT-компилятора JVM, в данном случае их может оказаться недостаточно. Будущие запланированные оптимизации могут опираться не только на неизменяемость в течение жизни процесса, но и на неизменяемость полей от одного запуска приложения к следующему.
-
Вместо того чтобы указывать модули, код которых может изменять final-поля, мы могли бы просить разработчиков указывать модули, которые разрешают изменять свои final-поля.
Изменение final-полей нежелательно, поэтому лучше фиксировать в командной строке модули, код которых следует обновить, чтобы он больше не пытался выполнять изменения. Если бы вместо этого указывались модули, final-поля которых можно изменять, это не фиксировало бы, почему они разрешают изменять свои поля, и не побуждало бы библиотеки отказываться от изменения этих полей.
-
Мы могли бы требовать, чтобы в
--enable-final-field-mutationуказывался и модуль, выполняющий изменение, и модули, содержащие изменяемые поля.Это было бы излишне обременительно. Во многих практических случаях
--enable-final-field-mutationбудет указываться вместе с--add-opens, где уже указаны обе стороны, участвующие в использовании глубокой рефлексии.