PunchNations League: Tuchel recalls Alexander-Arnold, Palmer as England name 27-man squadESPNTransfer rumors, news: Arsenal eye versatile Dortmund forwardCNN TürkAltında yönü faiz beklentileri belirleyecek!Bollywood HungamaSamay Raina, Medha Shankr BREAK silence on December 2026 wedding reports; comedian quips, “Sorry, will have to postpone”InquirerCalabarzon cops on full alert for martial law protestsUOLPodcast: a reta final da corrida eleitoral e as preocupações de Lula e Flávio BolsonaroCapital FM‘We have a better model than Uganda,’ Ruto defends G-to-G fuel deal7sur7Un chien enlevé à Hal a été retrouvé dans une voiture à Schaerbeek: “On voyait qu’il était traumatisé”ABC News (Australia)Woman given 'too many drugs, too little care' before death, coroner findsBBC NewsPalmer set to withdraw from England squad after recallTRT Haberİmalat sanayisinde kapasite kullanım oranı eylülde yüzde 74,2 olduDW 中文川習會四大重點:中美盤算甚麼?台灣會否成籌碼?
The Daily Newsstand · Free, Always
Monday, September 21, 2026

Final должен быть final

Translate

В JDK 26 есть JEP 500 — Prepare to Make Final Mean Final. Теперь JVM предупреждает о попытках изменить через deep reflection final-поле объекта, не объявленное как static. В одном из будущих релизов такие операции планируется запрещать, если владелец приложения не разрешил их явно. В статье разберём:

  • какие гарантии даёт final языку, модели памяти и JVM;

  • как reflection позволяет обойти эти гарантии;

  • какие фреймворки могут зависеть от такого поведения;

  • как подготовить проект к более строгому режиму.

Что означает final для JVM (JLS 17.5)

final-поле объекта инициализируют при объявлении или в конструкторе, и после этого присвоить ему другое значение обычными средствами Java нельзя. Однако запрет повторного присваивания — лишь часть семантики: такие поля участвуют в правилах Java Memory Model и дают JVM информацию для оптимизации кода.

При завершении конструирования объекта происходит freeze action, которое связывает запись внутри конструктора с последующими чтениями из других потоков. Благодаря этому Java Memory Model гарантирует, что другие потоки увидят значение final-поля даже без отдельной синхронизации — но только при корректной публикации объекта, т.е. если в конструкторе не происходит передачи this в другой поток или обработчик, способный выполниться до окончания инициализации. В HotSpot требуемая видимость обеспечивается барьером памяти, который компилятор добавляет перед нормальным выходом из конструктора (это деталь реализации, а не требование спецификации).

Как final помогает JIT оптимизировать код

Модификатор final в Java может влиять на оптимизации, которые выполняет JIT-компилятор (в частности, C2). Однако его воздействие зависит от того, где именно он применяется.

Для локальных переменных и параметров final практически не даёт JIT-компилятору дополнительной информации. Он лишь запрещает повторное присваивание на уровне исходного кода, что проверяет javac. C2 и без этого строит граф потока данных и отслеживает, откуда приходит каждое значение и где оно используется. Поэтому локальный final не расширяет возможности оптимизаций.

Основной интерес для C2 представляют именно final-поля объектов. Чтение обычного (не final) поля – это загрузка из памяти. Между двумя чтениями поле могло быть изменено. Если компилятор не может доказать отсутствие таких записей, он вынужден каждый раз выполнять реальное обращение к памяти.

С final-полем ситуация иная: во внутренней модели памяти C2 такое поле помечается как “не перезаписываемое”.

Это даёт несколько важных оптимизаций:

  1. Устранение повторных чтений. C2 может загрузить значение final-поля один раз и использовать его во всех местах метода, где оно требуется. Это сокращает число обращений к памяти и позволяет дольше хранить значение в регистре процессора.

  1. Вынос чтения из циклов. Если значение final-поля не меняется, его не нужно загружать на каждой итерации. Компилятор может прочитать его перед циклом и использовать внутри.

  1. Более раннее выполнение чтения. Записи в другие поля не могут изменить значение final-поля, поэтому C2 не обязан ждать их завершения, чтобы прочитать final-поле. Это уменьшает число зависимостей между операциями с памятью и даёт больше свободы при планировании инструкций.

Таким образом, final снижает количество барьеров памяти и позволяет компилятору агрессивнее переупорядочивать операции.

Следует понимать, что final-поле объекта не обязано быть константой. У разных экземпляров класса в этом поле могут лежать разные значения. Кроме того, во время компиляции конкретного метода C2 часто не знает, с каким именно объектом он будет вызван. Поэтому он не может просто подставить значение поля как константу — он лишь уверен, что для данного объекта оно не изменится в течение жизни метода.

Подстановка конкретного значения (constant folding) возможна только в случаях, когда компилятор может определить конкретный объект (например, после инлайнинга конструктора) или когда поле принадлежит классам, для которых мутация через reflection запрещена. К таким классам относятся record и скрытые классы (hidden classes).

Отдельный случай — static final поля. После инициализации класса их значения могут рассматриваться как настоящие константы времени компиляции. Более того, такие поля нельзя изменить даже через Field::set. Поэтому C2 может смело подставлять их значения в код.

Как final меняют через reflection

import java.lang.reflect.Field;

public final class FinalMutationDemo {

    static final class Token {

        private final int value;

        Token(int value) { this.value = value; }

        int value() { return value; }

        @Override public String toString() { return "Token[value=" + value + "]"; }

    }

    public static void main(String[] args) throws Exception {

        Token token = new Token(100);

        Field field = Token.class.getDeclaredField("value");

        field.setAccessible(true);

        System.out.println("before: " + token);

        field.setInt(token, 200);

        System.out.println("after:  " + token);

    }

}

Результат выполнения:

before: Token[value=100]
after:  Token[value=200]

В примере мы получаем доступ к приватному полю, делаем его доступным и меняем значение уже после создания объекта.

В модульной системе (Java 9+) одного setAccessible(true) может быть недостаточно — целевой модуль должен быть открыт для вызывающего модуля через opens или --add-opens.

Коротко о opens и --add-opens. В модульной системе Java по умолчанию рефлексивный доступ к непубличным членам классов из другого модуля запрещён. Чтобы его разрешить, нужно либо указать opens в module-info.java для нужного пакета, либо передать при запуске параметр --add-opens <модуль>/<пакет>=<целевой модуль>. Эти механизмы дают возможность вызвать setAccessible(true) и получить доступ к полю. Но они не разрешают изменение final-полей.

Зачем такой доступ понадобился фреймворкам

Сериализаторам, ORM, контейнерам dependency injection и тестовым инструментам приходится работать с классами, которые проектировались без учёта требований конкретного фреймворка. 

Им может понадобиться:

  • создать объект без публичного конструктора;

  • восстановить объект из байтов или строки базы данных;

  • заполнить закрытые поля;

  • подменить зависимость в тесте;

  • поддержать старую модель без конструктора или фабричного метода с нужными параметрами.

Поэтому reflection и Unsafe стали использоваться для обхода инкапсуляции и прямого изменения состояния объекта.

Какие проблемы создаёт мутация final

В конструкторе обычно проверяют входные данные и не позволяют создать объект с недопустимым состоянием. После создания объекта остальной код рассчитывает, что его final-поля больше не изменятся. Reflection позволяет заменить значение в обход этих проверок.

Само наличие reflection-записи ещё не означает уязвимость. Но такой механизм позволяет нарушить инварианты, на которых может строиться проверка прав, выбор стратегии, конфигурация или валидация данных. Поэтому неконтролируемая мутация final-полей увеличивает поверхность атаки и затрудняет аудит кода.

Что изменилось в JDK 26

При запуске кода с мутацией final-поля JVM выведет предупреждение:

WARNING: Final field value in class com.example.class has been mutated reflectively by class com.example.class in unnamed module @5674cd4d (file:/.../target/classes/)
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 26 добавили параметр --illegal-final-field-mutation:

  • allow — выполнить запись без предупреждения;

  • warn — выполнить запись и вывести первое предупреждение для каждого модуля;

  • debug — при каждой записи вывести предупреждение и stack trace;

  • deny — отклонить запись: Field::set выбросит IllegalAccessException.

Пока по умолчанию используется режим warn. В будущем deny должен применяться без дополнительного параметра JVM, но конкретный релиз в JEP 500 не указан.

Также добавили флаг, позволяющий временно разрешить мутацию для кода:

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

Для модульного приложения можно перечислить конкретные модули:

java --enable-final-field-mutation=my.module,my.other.module -m my.app/com.example.Main

Этот параметр разрешает выполнять мутацию коду из указанных модулей. Он не открывает целевой пакет автоматически: правила opens и --add-opens продолжают действовать.

Такой флаг — средство совместимости на время миграции.

Что делать в проекте

Найти проблемные места

Сначала приложение следует запустить на JDK 26 в диагностическом режиме:

java --illegal-final-field-mutation=debug MyApp

В этом режиме каждая попытка изменить final-поле сопровождается stack trace. По нему можно определить библиотеку и путь вызовов, который привёл к записи.

Для длительного или нагрузочного запуска можно использовать JFR:

java -XX:StartFlightRecording:filename=recording.jfr MyApp

jfr print --events jdk.FinalFieldMutation recording.jfr

Исправить модель объекта

Вместо мутации final-полей инициализируйте их в конструкторе, статической фабрике или билдере. Для Spring используйте constructor injection, в тестах передавайте моки через конструктор.

Плюсы такого решения:

  • инварианты проверяются в одном месте;

  • значение final-поля действительно задаётся при создании объекта;

  • код не зависит от устаревающего обходного механизма.

Если запись выполняет библиотека, нужно проверить её актуальную версию и настройки. Сериализаторы часто позволяют выбрать конструктор, фабричный метод, билдер или пользовательский адаптер. Для ORM может потребоваться изменить модель хранения или отделить entity от неизменяемой доменной модели.

Выводы

final в Java — это не только запрет повторного присваивания в исходном коде. На нём основаны гарантии видимости Java Memory Model, инварианты объектов и часть решений, которые может принимать JIT-компилятор.

JDK 26 делает скрытую reflection-запись заметной: операция пока выполняется, но JVM выводит предупреждение. В одном из будущих релизов такая запись должна завершаться IllegalAccessException, если владелец приложения не разрешил её явно.

JEP 500 укладывается в более общий курс OpenJDK — integrity by default: опасные возможности должны быть видимыми и включаться явно. Поэтому ждать момента, когда режим deny начнёт применяться по умолчанию, не стоит. Проект уже можно проверить с --illegal-final-field-mutation=debug, временные разрешения зафиксировать как технический долг, а собственный код перевести на инициализацию final-полей в конструкторах, фабриках или билдерах.

Дополнительный разбор миграции опубликован в блоге Inside Java: Avoiding final field mutation.

View the original on Хабр

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.