JEP 539: Strict Field Initialization in the JVM (Preview)
Strict Field Initialization (строгая инициализация полей) в JVM, версия Preview (предварительная версия)
| Ответственный | Dan Smith |
| Тип | Feature |
| Область | SE |
| Статус | Integrated |
| Выпуск | 28 |
| Обсуждение | valhalla dash dev at openjdk dot org |
| Трудоёмкость | M |
| Длительность | L |
| Связан с | JEP 401: Value Objects (Preview) |
| Рецензенты | Alex Buckley, Brian Goetz |
| Одобрен | Brian Goetz |
| Создан | 2025/02/20 21:25 |
| Обновлён | 2026/08/07 18:03 |
| Задача | 8350458 |
Аннотация
Ввести в Java Virtual Machine строго инициализируемые поля. Такие поля должны быть инициализированы до того, как их прочитают, поэтому значения по умолчанию, такие как 0 или null, никогда не наблюдаются. Для строго инициализируемых полей, объявленных как final, всегда наблюдается одно и то же значение. Это Preview-возможность JVM, доступная компиляторам, которые генерируют class-файлы.
Цели
-
Предложить разработчикам языков программирования на базе JVM модель инициализации полей с более сильными гарантиями целостности, чем у текущей модели.
-
Дать этим разработчикам возможность выбирать для каждого статического поля и поля экземпляра в классе, переходить ли на новую модель или продолжать использовать текущую.
Что не является целью
-
Не является целью вводить новые возможности языка Java, такие как модификатор строгой инициализации для полей.
-
Не является целью менять стратегии компиляции
javac, чтобы применить Strict Field Initialization к существующему исходному коду на Java.
Мотивация
Платформа Java требует, чтобы каждая переменная была инициализирована до использования: так программа никогда не может прочитать неинициализированную память. Если поле класса — статическое или поле экземпляра — не инициализировано явно, оно инициализируется неявно до использования: ему присваивается значение по умолчанию. Это значение всегда является той или иной формой нуля: число 0, логическое значение false или ссылка null.
Значения по умолчанию приносят как пользу, так и вред. Они дают простую страховку, гарантируя, что программа никогда не увидит неинициализированную память, но их часто можно ошибочно принять за настоящие данные, а не за сигнал того, что туда ещё ничего не записано.
Например, метод может прочитать из поля значение null и передать его другим методам и конструкторам, а в итоге это вызовет NullPointerException где-то далеко от места, где поле было прочитано. В JDK 14 сообщения таких исключений были улучшены, чтобы проще находить источник ошибки в конкретной строке кода, но эти сообщения не могут указать на ошибку инициализации, из-за которой изначально появилось значение null.
Платформа Java также требует, чтобы переменные, объявленные как final, нельзя было изменить: любые два чтения переменной final дают одно и то же значение. Однако для final-полей это правило не действует, пока класс или экземпляр инициализируется. Поэтому программа может в разное время прочитать разные значения, пока полям присваиваются предназначенные для них значения.
Ошибки инициализации полей на практике
Следующий пример показывает проблемы неожиданных значений по умолчанию и несогласованных final-полей. В этих классах final-поле App.appID может быть прочитано кодом класса Log до того, как ему присвоено правильное значение. Когда это происходит, разные компоненты программы в итоге работают с противоречащими друг другу значениями поля.
class App {
public static final long appID = Log.currentPID(); // [1], [4], [6]
public static void main() {
IO.println("App[" + appID + "] has started");
// ...
Log.log("Completed 'main'");
}
}
class Log { // [2]
private static final String prefix = "App[" + App.appID + "]: "; // [3]
public static void log(String msg) {
IO.println(prefix + msg);
}
public static long currentPID() {
return ProcessHandle.current().pid(); // [5]
}
}
Если запустить класс App из командной строки, вывод будет примерно таким:
App[96052] has started
App[0]: Completed 'main'
Расхождение между ID-номерами возникает потому, что вызов Log.currentPID() в классе App [1] запускает инициализацию класса Log [2], а во время инициализации этого класса читается значение по умолчанию 0 поля appID [3], и оно встраивается в строку prefix. После того как класс Log инициализирован, продолжается вызов его метода currentPID из класса App [4], который возвращает ID-номер текущего процесса [5], и этот номер наконец присваивается App.appID [6]. Однако для поля prefix это присваивание происходит слишком поздно.
В сложных системах такие ошибки трудно распознать и диагностировать. Одна тонкость в том, что важен порядок инициализации: если класс Log инициализируется первым, расхождения не видно. Другая тонкость в том, что циклическую зависимость между классами App и Log легко создать по ошибке и легко потом не заметить; если бы вспомогательный метод currentPID был объявлен в каком-то другом классе, цикла бы не было, и всё работало бы как ожидается.
Большинство видов переменных в Java не страдают от этих проблем. Локальной переменной нужно явно присвоить значение до её чтения, а final-локальной переменной значение можно присвоить только один раз. Только поля полагаются на значения по умолчанию.
Строгий подход к инициализации полей
Мы предлагаем другой подход к инициализации полей, как не-final, так и final. Вместо того чтобы при создании каждого поля инициализировать его значением по умолчанию, мы изменяем JVM так, чтобы некоторые поля, помеченные как строго инициализируемые, явно инициализировались в байт-коде, прежде чем их будет разрешено читать. Компиляторы, такие как javac, отвечают за выбор того, какие поля помечаются как строго инициализируемые, в зависимости от возможностей языка, используемых в исходном коде. Мы называем это Strict Field Initialization, потому что это накладывает дополнительные ограничения на код, инициализирующий поля.
Strict Field Initialization исключает неожиданные значения по умолчанию и несогласованные final-поля. Каждое чтение строго инициализируемого поля видит ранее записанное значение, а если поле final, каждое чтение видит одно и то же значение. Именно этих свойств мы уже интуитивно ожидаем от полей; Strict Field Initialization превращает эти свойства из простых интуитивных ожиданий в настоящие гарантии целостности, которые обеспечивает JVM.
Strict Field Initialization повышает целостность
Strict Field Initialization закладывает основу для новых возможностей языка Java:
-
Value Classes (классы-значения) — новый вид классов, экземпляры которых не обладают Identity (идентичность объекта) и никогда не могут быть изменены. Крайне важно, чтобы final-поля экземпляра value-класса всегда имели одно и то же наблюдаемое значение.
-
В будущем некоторые поля, как статические, так и поля экземпляра, могут не иметь возможности хранить
null. Такие поля не смогут полагаться на значения по умолчаниюnullв JVM. Вместо этого их нужно будет явно инициализировать значением, отличным отnull, прежде чем их можно будет прочитать.
Как показано выше, процесс инициализации полей бывает хрупким. JVM не должна навязывать новое поведение инициализации существующим программам, поскольку они могут зависеть от текущего поведения. Новые возможности языка, напротив, могут определять новые правила и поведение инициализации полей и затем применять Strict Field Initialization. По мере развития языка и внедрения новых возможностей компоненты программ будут постепенно становиться защищёнными от ошибок инициализации полей.
Описание
У строго инициализируемого поля нет значения по умолчанию. Его нельзя прочитать до явной инициализации, а если оно final, все чтения дают одно и то же значение. Компиляторы помечают поля, подлежащие строгой инициализации, новым флагом в class-файле — ACC_STRICT_INIT (0x0800).
Для строго инициализируемых полей JVM обеспечивает следующие инварианты:
-
Для поля экземпляра чтение не может обратиться к полю до вызова конструктора
super(), а поле должно быть инициализировано до вызова конструктораsuper(). Если поле final, запись не может изменить поле после вызова конструктораsuper(). Нарушение любого из этих ограничений приводит к ошибке верификации байт-кода.
Инварианты строго инициализируемых полей дают JVM новые возможности для оптимизации работы с этими полями. Например, JIT-компилятор HotSpot JVM будет считать строго инициализируемые final-поля доверенными. Известно, что доверенное final-поле никогда не меняется, поэтому после того как из него прочитано значение, последующие чтения могут повторно использовать то же значение. В результате JIT-скомпилированный код меньше обращается к памяти и может работать быстрее.
Ниже мы рассмотрим процесс инициализации классов в JVM и подробнее обсудим новые правила для строго инициализируемых статических полей. Затем мы рассмотрим процесс инициализации экземпляров и обсудим новые правила для строго инициализируемых полей экземпляра.
Это Preview-возможность JVM, по умолчанию отключённая
Флаг ACC_STRICT_INIT, обозначающий строго инициализируемое поле, распознаётся только в class-файлах с номером версии Preview (72.65535) и только когда Preview-возможности включены во время выполнения.
Чтобы включить Preview-возможности во время выполнения, используйте параметр командной строки --enable-preview:
$ java --enable-preview Main
Value Classes — новая возможность языка Java — опираются на Strict Field Initialization: компиляторы помечают все поля value-классов как ACC_STRICT_INIT. Чтобы программировать с использованием Value Classes, нужно включить Preview-возможности и при компиляции, и во время выполнения, чтобы включить и Value Classes, и Strict Field Initialization.
Strict Field Initialization — самостоятельная возможность JVM. Она не предполагает существования Value Classes и может использоваться компиляторами других языков, кроме Java. Независимо от компилятора, class-файлы с полями, помеченными как ACC_STRICT_INIT, могут быть загружены, только если Preview-возможности включены во время выполнения.
Инициализация классов сегодня
Каждый раз, когда JVM загружает класс, его необходимо инициализировать. Для этого в байт-коде класс или интерфейс может объявить метод инициализации класса с именем <clinit>. Метод инициализации класса может выполнять произвольный код. Обычно инициализация класса включает присваивание всем статическим полям класса подходящих начальных значений; она может также включать взаимодействие с глобальным состоянием.
В исходном коде на Java метод инициализации класса не пишется напрямую; скорее, он представляет собой объединение инициализаторов статических полей и статических блоков инициализации класса.
Каждый класс в иерархии может иметь собственный метод <clinit>. Каждый суперкласс должен быть инициализирован до выполнения метода <clinit> подкласса.
Класс, инициализация которого началась, но ещё не завершилась, считается larval. Он развивается, но ещё не полностью сформирован.
JVM отслеживает состояние инициализации каждого класса во время выполнения. В сегодняшней JVM (см. JVMS §5.5) состояние инициализации класса может быть одним из следующих:
-
Uninitialized: класс загружен, но инициализация ещё не началась.
-
Larval (в рамках конкретного потока): класс в данный момент инициализируется.
-
Initialized: класс успешно завершил инициализацию и может использоваться без ограничений.
-
Erroneous: инициализация класса завершилась ошибкой, и его нельзя использовать.
Метод <clinit> выполняется, пока класс находится в состоянии larval. В этот момент класс ещё не инициализирован, но код, выполняющийся в текущем потоке, может свободно обращаться к его полям и методам. Если метод <clinit> завершается успешно, класс переходит в состояние initialized. Если выбрасывается исключение, класс переходит в состояние erroneous и уже никогда не сможет стать инициализированным.
Ограничения инициализации классов проверяются динамически, во время выполнения. Например, каждая инструкция getstatic проверяет состояние инициализации класса разрешённого поля. Если класс не инициализирован, но находится в состоянии larval в другом потоке, инструкция getstatic блокируется, пока инициализация не завершится.
Строгая инициализация статических полей
Чтобы реализовать строгую инициализацию статических полей, мы расширяем состояние инициализации класса larval так, чтобы оно отслеживало, было ли каждое статическое поле класса установлено и было ли каждое статическое поле класса прочитано.
При выполнении инструкции putstatic или getstatic, если разрешённое поле объявлено классом, который находится в состоянии larval в текущем потоке, состояние обновляется: в нём отмечается, что поле было установлено (инструкцией putstatic) или прочитано (инструкцией getstatic). Это происходит, даже если к полю обращаются из другого метода или класса, и даже если к полю обращаются через подкласс.
Поле, объявленное с атрибутом ConstantValue, всегда считается установленным.
Располагая этой информацией, JVM может обеспечить соблюдение инвариантов строго инициализируемых статических полей:
-
Если инструкция
getstaticпытается прочитать строго инициализируемое поле, объявленное классом в состоянии larval, и это поле ещё не установлено, JVM выбрасывает исключение, сообщающее, что поле пока нельзя прочитать. -
Если инструкция
putstaticпытается записать строго инициализируемое final-поле, объявленное классом в состоянии larval, и это поле уже было прочитано, JVM выбрасывает исключение, сообщающее, что поле больше нельзя установить. -
Непосредственно перед переходом класса в состояние initialized его состояние larval проверяется, чтобы убедиться, что каждое строго инициализируемое статическое поле установлено; если это не так, JVM выбрасывает исключение, указывая одно из полей, которые должны быть явно установлены во время инициализации класса.
(В некоторых сложных случаях, например при обработке исключений, статическое final-поле может быть записано несколько раз во время инициализации. Это допускается, но прочитать можно будет только окончательное значение поля.)
Приведённые выше правила соблюдаются, даже если статическое поле читается или записывается рефлексивно во время инициализации класса, например через API java.lang.reflect.Field или java.lang.invoke.VarHandle.
Инициализация экземпляров сегодня
Каждый раз, когда экземпляр класса создаётся байт-кодом new, этот экземпляр должен быть инициализирован. Для этого в байт-коде класс может объявить несколько методов инициализации экземпляра с именем <init>. Эти методы могут выполнять произвольный код. С помощью цепочки вызовов методов <init> каждый класс в иерархии наследования определяет, что представляет собой инициализированный экземпляр класса. Обычно инициализация экземпляра включает установку всех полей экземпляра объекта в подходящие начальные значения; она также может включать взаимодействие со статическими полями класса или другим глобальным состоянием.
В исходном коде на Java методы инициализации экземпляра в основном выражаются конструкторами, а делегирование между конструкторами — вызовами super(...) и this(...). Методы инициализации экземпляра могут также включать код из инициализаторов полей экземпляра и блоков инициализации экземпляра класса.
У каждого класса в иерархии есть по крайней мере один метод <init>, и этот метод должен в какой-то момент до своего завершения делегировать другому методу <init> либо текущего класса, либо его суперкласса. Эта рекурсия заканчивается на Object::<init>.
Экземпляр, инициализация которого началась, но ещё не завершилась, как и класс, считается находящимся в состоянии larval. Он развивается, но ещё не сформирован полностью.
Как и у классов, у экземпляров есть состояние инициализации, хотя в JVM Specification оно выражено лишь косвенно. Сегодня состояние инициализации объекта может быть одним из следующих:
-
Uninitialized (не инициализирован): объект создан инструкцией
new, но инициализация ещё не началась. -
Early larval (ранняя стадия larval): объект сейчас инициализируется, и доступны лишь ограниченные операции.
-
Late larval (поздняя стадия larval): объект сейчас инициализируется, но уже достаточно сформирован, чтобы его можно было использовать без ограничений.
-
Initialized (инициализирован): объект успешно завершил инициализацию.
-
Erroneous (ошибочное состояние): инициализация объекта завершилась неудачей, и его нельзя использовать.
Метод <init> начинает выполнение в состоянии early larval. Большинство операций, включая вызовы методов, над объектом в состоянии early larval запрещены, и объект нельзя передавать другому коду. Однако его полям можно присваивать значения с помощью putfield. В конце концов вызывается другой метод <init>, и процесс инициализации рекурсивно продолжается, пока не дойдёт до Object::<init>. В этот момент экземпляр переходит в состояние late larval, и рекурсивно вызванные методы <init> один за другим завершают выполнение и возвращают управление. В состоянии late larval использование объекта, включая его поля и методы, не ограничено; объект можно даже передавать между потоками. Объект считается инициализированным, как только самый внешний метод <init> успешно возвращает управление. Иначе любой вызов <init> в стеке может завершиться исключением; в этом случае объект переходит в состояние erroneous и уже никогда не сможет стать инициализированным.
Ограничения инициализации экземпляров проверяются статически, верификатором байт-кода. Верификация определяет для каждой инструкции состояние типов, которое может быть ограниченным (для кода, работающего с экземпляром в состоянии early larval) или неограниченным (для кода, работающего с экземпляром в состояниях late larval и initialized, а также для кода в статических методах).
Для инструкций с ограниченным состоянием типов верификатор запрещает большинство операций над текущим объектом. Он также гарантирует, что неограниченного состояния типов можно достичь только через цепочку рекурсивно делегирующих вызовов <init>, которая в итоге доходит до Object::<init>. Инструкция return, которая делает только что созданный объект доступным вызывающему <init>, разрешена только в неограниченном состоянии типов.
Strict Field Initialization для полей экземпляра
Чтобы реализовать строгую инициализацию полей экземпляра, мы расширяем состояние инициализации экземпляра early larval так, чтобы оно отслеживало, было ли каждое поле экземпляра класса установлено.
В верификаторе это выражается ограниченным состоянием типов, которое содержит список всех ещё не установленных строго инициализируемых полей экземпляра текущего класса. Инструкция putfield над текущим экземпляром класса в ограниченном состоянии типов удаляет указанное поле из списка.
Расширенное состояние типов поддерживает следующие правила, обеспечивающие соблюдение инвариантов строго инициализируемых полей экземпляра:
-
Для инструкции
invokespecialметода<init>, применённой к текущему экземпляру класса в ограниченном состоянии типов, требуется, чтобы при вызове метода суперкласса список неустановленных полей был пуст. (Если вызывается другой метод<init>того же класса, такого требования нет — за установку полей отвечает вызванный метод.) -
Инструкция
putfield, записывающая строго инициализируемое final-поле текущего класса, разрешена только в ограниченном состоянии типов. (Для сравнения: для final-полей, которые не инициализируются строго,putfieldразрешена во всём теле метода<init>.)
Использовать getfield для экземпляра в ограниченном состоянии типов никогда не разрешалось. Поэтому для getfield нет правила, аналогичного правилу для getstatic в случае статических полей, и нет необходимости отслеживать, были ли прочитаны final-поля.
Переходы между ограниченными и неограниченными состояниями типов запрещены. Переходы между разными ограниченными состояниями типов разрешены, если переход ведёт в состояние типов, в котором установлено меньше полей.
Эти правила верификации гарантируют, что все строго инициализируемые поля объекта устанавливаются, пока он находится в состоянии early larval, до того как может произойти любое чтение, и что никакие строго инициализируемые final-поля не изменяются после того, как объект перешёл в состояние late larval. При выполнении верифицированного кода дополнительные проверки во время выполнения для соблюдения инвариантов инициализации не нужны.
В class-файле атрибут StackMapTable выражает ожидаемое входящее состояние типов для цели перехода. Раньше ограниченное состояние типов выражалось просто включением специального типа uninitializedThis в список локальных переменных. Но если у класса есть строго инициализируемые поля, состоянию типов может также понадобиться указывать, установлено ли каждое поле. Для этого используется новый вид записи фрейма StackMapTable:
early_larval_frame {
u1 frame_type = EARLY_LARVAL; /* 246 */
u2 number_of_unset_fields;
u2 unset_fields[number_of_unset_fields];
// array of NameAndType constants
base_stack_map_frame base_frame;
// any other kind of stack frame
}
Если же у фрейма стека любой другой frame_type, но в нём упоминается uninitializedThis, фрейм стека неявно считается ограниченным, а неустановленными считаются те поля, которые были не установлены в предыдущем фрейме.
Строго инициализируемые final-поля нельзя изменить с помощью deep reflection
Некоторые приложения и фреймворки используют deep reflection, реализованную в методах setAccessible и set API [java.lang.reflect.Field], чтобы изменять приватные или final-поля объекта после завершения инициализации экземпляра. В JDK 26 изменение final-полей с помощью deep reflection разрешено, но вызывает предупреждение; в будущем выпуске тем, кому нужна эта возможность, придётся явно включать её при запуске. (Подробнее см. JEP 500.)
Изменение строго инициализируемых final-полей с помощью deep reflection противоречит инвариантам Strict Field Initialization: разные чтения одного и того же final-поля могли бы видеть разные значения. Поэтому метод setAccessible относит такие поля к неизменяемым, так же как статические final-поля и final-поля классов Records (записи). Попытка вызвать set для строго инициализируемого final-поля всегда выбрасывает IllegalAccessException. Использование --enable-final-field-mutation=... не позволит изменять эти неизменяемые поля.
Чтобы установить строго инициализируемое final-поле экземпляра класса, вы должны использовать один из конструкторов класса: только они могут присваивать значение этому полю.
Строго инициализируемым полям нужна собственная десериализация
Десериализация объектов, реализованная в API ObjectInputStream, пропускает обычное выполнение метода <init> в создаваемом классе. Вместо этого API само создаёт объект с помощью рефлексивного библиотечного кода. Как и deep reflection, эта возможность обходит основанную на верификации проверку ограничений для строго инициализируемых полей экземпляра, и её нельзя использовать для классов, в которых объявлены такие поля.
Поэтому методы ObjectOutputStream::writeObject и ObjectInputStream::readObject выбрасывают InvalidClassException, если сериализуемый или десериализуемый класс объявляет строго инициализируемое поле экземпляра и не является классом Records.
Чтобы избежать этого исключения, реализуйте методы writeReplace и readResolve. Тогда вместо объекта со строго инициализируемыми полями будет сериализоваться и десериализоваться объект-заменитель.
(Мы ожидаем в будущем улучшение сериализации, которое позволит вам указать код создания объектов, с помощью которого ObjectInputStream::readObject сможет безопасно создавать новые экземпляры из данных потока сериализации. Этот процесс будет опираться на обычный вызов конструктора и поэтому будет совместим со строго инициализируемыми полями экземпляра.)
Сопутствующие изменения
-
В классе
java.lang.reflect.Fieldсуществующий методaccessFlagsи новый методisStrictInitотражают наличие флагаACC_STRICT_INITу полей. -
API
java.lang.classfileподдерживает флаг доступаACC_STRICT_INITу полей и записиearly_larval_frameв атрибутахStackMapTable. КогдаStackMapTableгенерируется автоматически для метода<init>, он правильно кодирует состояние строго инициализируемых полей экземпляра. -
Инструмент
javapпоказывает модификаторACC_STRICT_INITи записиearly_larval_frame; он также показывает неявные неустановленные поля других записейStackMapTable. -
Утилиты AsmTools аналогично поддерживают флаг
ACC_STRICT_INITи записиearly_larval_frame. -
Код JNI, как обычно, может нарушать инварианты JVM. Никаких особых ограничений на его возможность читать и изменять области памяти строго инициализируемых полей нет. (Но см. JEP 472 об изменениях платформы, которые способствуют целостности по умолчанию.)
-
JVM Tool Interface не позволяет досрочно возвращаться из методов
<init>, которым нужно установить строго инициализируемые поля.
Альтернативы
-
Поля с атрибутом
ConstantValue, давно существующей возможностью JVM, можно считать уже строго инициализируемыми: заданное значение присваивается полю до того, как какой-либо пользовательский код попытается его прочитать. Но этот атрибут работает только для статических полей примитивного типа или типаStringи, что неудивительно, может присваивать только константные значения. Во многих сценариях строгой инициализации полей нужно, чтобы начальные значения можно было получать из параметров конструктора или вычислять с помощью байт-кода общего назначения. -
В JDK 21 компилятор
javacначал выдавать предупреждения, чтобы отговорить от вызовов методов экземпляра из конструкторов суперкласса. Эти предупреждения помогают не допустить, чтобы объекты в состоянии late-larval становились доступны для общего использования до того, как их поля будут корректно инициализированы:class Parent { Parent() { super(); // warning: 'this' may not be fully initialized: OtherClass.foo(this); } } class Child extends Parent { String s; Child(String s) { super(); this.s = s; } }Предупреждения об обращении с объектами в состоянии late-larval полезны, но предупреждения можно игнорировать, а автор подкласса не всегда может контролировать соглашения о написании кода, которые соблюдаются в суперклассе. Strict Field Initialization вместо этого требует, чтобы поля получали значения, пока объект находится в состоянии early-larval, до того, как появится какая-либо возможность утечки объекта во внешний код.
-
В некоторых ситуациях вам может понадобиться динамически гарантировать, что поле инициализируется до того, как оно будет прочитано, но без необходимости вычислять значение поля в момент инициализации. Вместо того чтобы усложнять этим JVM, такое поведение лучше всего обеспечивать с помощью библиотек.
Например, можно использовать Lazy Constants (ленивые константы), чтобы смоделировать final-переменную с кодом инициализации, который выполняется по требованию, при первой попытке её прочитать:
class Constants { final LazyConstant<String> s = LazyConstant.of(() -> lazyInitializer()); }
Риски и допущения
-
Новые возможности JVM обходятся дорого. Мы ожидаем, что у Strict Field Initialization будет несколько значимых сценариев использования, которые вместе оправдают её стоимость. Однако это зависит от успеха новых возможностей языка, которые опираются на новые гарантии целостности, например тех, что рассмотрены выше. Это также зависит от того, будут ли разработчики готовы переходить на альтернативы традиционной последовательности инициализации экземпляра сверху вниз.
-
Есть небольшой риск, что существующие инструменты могут по ошибке установить у поля флаг
ACC_STRICT_INIT. Значение флага доступа0x0800исторически использовалось для обозначения методовstrictfp, которые включали особую строгую семантику операций с плавающей точкой, ставшую устаревшей в Java 17. Однако вероятность путаницы невелика, посколькуstrictfpимеет значение только в class-файлах версии60или более ранних, аACC_STRICT_INITимеет значение только в class-файлах версии72.65535.