Введение
Скоро в Java (предположительно, что уже непосредственно в JDK 26) попадут изменения, которые сначала будут выдавать warning-и при попытках изменения final полей, а потом и вовсе запретят изменения final полей в общем случае (это уже, предположительно, в следующих релизах).
Я периодически катаюсь на различные европейские конференции, и уже довольно давно на них от ключевых архитекторов Java, таких как Brian Goetz, Stuart Marks (довольно известный на конференциях по прозвищу "Dr. Deprecator"), Mark Reinhold и др. превалировал сентимент, который впоследствии выразился в идеологию под названием "Integrity By Default" ("Целостность по умолчанию"). В рамках Spring АйО мы уже как-то вскользь о ней упоминали, но не формализовывали. Я предлагаю сначала начать с неё, и потом уже раскручивать непосредственно всю эпопею с final в Java.
Integrity By Default. Откуда ноги растут
"Integrity By Default" это не конкретный JEP, не конкретное изменение в Java платформе, в языке, в VM и т.д. Это скорее общий вектор развития, и общая политика, которой руководствуются разработчики платформы Java. Довольно хорошее формальное определение подходу "Integrity By Default" я находил в презентации Рона Пресслера, который также довольно плотно работает над Java платформой (Если я не ошибаюсь, презентация была для доклада на JVM Language Summit 2023, но это не точно):
Integrity by Default: Every exception to integrity must be explicitly acknowledged by the application in a centralized program configuration
Я думаю, что формулировка выше довольно хорошо объясняет общий подход. И, соответственно, JEP 500 один из шагов к тому, чтобы Java-платформа работала в соответствии с "Integrity By Default". Иными словами, если разработчик написал
final int x = 42;
то наверное он имел в виду, что x всегда будет и должен быть 42. Он не имел в виду, что x = 42 на данный момент, но на деле его можно менять как угодно и когда угодно.
Возникает закономерный вопрос - а как же так вышло-то, что вообще final поля в Java можно менять? Для человека, который не имел шанса погрузиться в историю вопроса сам факт того, что final поля можно менять, кажется чем-то из ряда вон выходящим. Действительно, если уже язык одной рукой дает тебе средства для объявления неизменяемого состояния, то как минимум очень странно, что другой рукой он эту гарантию у нас забирает.
Как же так вышло? Давайте разбираться.
Начнём с небольшой исторической справки
Друзья, смотрите, архитекторы Java действительно очень аккуратно подходят к введению новых фич в язык, так как понимают принцип, золотое правило, описанное ещё Joshua Bloch-ом в книжке Effective Java:
«When in doubt — leave it out»
«You can always add, but you can never remove»
Этим во многом объясняется относительно недавняя ситуация со String Templates или тот факт, что Project Valhalla претерпевает уже который прототип за 10+ лет развития.
Итого: добавление в язык каких-то более менее крупных фич это всегда трейд офф и к этому подходят очень аккуратно, так как можно наломать дров!
И вот в чём соль - дров уже наломали, например с final и с сериализацией!
Когда-то давно, когда я жевал в детском саде манную кашу и домогался до девчонок в ясельной группе (речь про воспитательниц), в Java, наряду с RMI (Remote Method Invocation) завезли сериализацию (ObjectInputStream , Serializable и т.п). И сериализация была такой, как предполагалось, "киллер-фичей". Её активно пиарили и, предполагалось, она будет являться одной из весомых причин выбора Java как платформы.
Конечно, сейчас мы понимаем, что у сериализации есть огромный ряд проблем:
Десериализация происходит путем создания "пустого" объекта (то есть вызова конструктора без аргументов, даже если вы его не определяли - VM может всё!). Это ломает integrity объекта - инварианты вашего конструктора в коде потенциально просто не будут соблюдены. Это ещё и огромный security exploit.
Создается очень сильный coupling между рантаймами на стороне как получателя, так и отправителя данных - формат сериализованного одной VM объекта должен быть понятен другой VM.
Отсутствие type-safety (объявляются какие-то "магические" методы для сериализации/десериализации
writeObject/readObject)
И на самом деле, ключево�� для нас сейчас следующее. Представим себе вот такой вот setup (Это простой Scratch-файл созданный в IDE):
class Scratch {
public static void main(String[] args) throws IOException, ClassNotFoundException {
var baos = new ByteArrayOutputStream();
var objectOutputStream = new ObjectOutputStream(baos);
Payment original = new Payment(Instant.now());
objectOutputStream.writeObject(original);
var objectInputStream = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()));
Payment deserialized = (Payment) objectInputStream.readObject();
System.out.println(original);
System.out.println(deserialized);
}
static class Payment implements Serializable {
private final String id;
private final Instant settledAt;
Payment(Instant settledAt) {
this.id = UUID.randomUUID().toString();
this.settledAt = settledAt;
}
@Override
public String toString() {
return "Payment{" +
"id='" + id + '\'' +
", settledAt=" + settledAt +
'}';
}
}
}
Исполняя данный main() я получаю примерно следующее (у вас, очевидно, output будет другой):
Payment{id='92477f74-45e4-4c73-873a-c6eca6ce19a7', settledAt=2025-11-10T16:02:45.899368Z}
Payment{id='92477f74-45e4-4c73-873a-c6eca6ce19a7', settledAt=2025-11-10T16:02:45.899368Z}
Суть в том, что в момент десериализации был вызван не конструктор класса Payment (в таком случае бы id в original отличался бы от id в deserialized). То есть, друзья, как можно понять, механизм десериализации установил поле id самостоятельно в обход конструктора, несмотря на то, что поле-то на самом деле final.
Я больше того скажу, чтобы мутировать final много ума не надо, и наш любимый Spring так тоже умеет:
public static void main(String[] args) {
Payment payment = new Payment(Instant.now());
Arrays.stream(Payment.class.getDeclaredFields()).filter(field -> {
return field.getName().equals("id");
}).findFirst().ifPresent(field -> {
field.setAccessible(true);
try {
Field modifiers = Field.class.getDeclaredField("modifiers");
modifiers.setAccessible(true);
modifiers.setInt(field, field.getModifiers() & ~Modifier.FINAL);
field.set(payment, null);
} catch (NoSuchFieldException | IllegalAccessException e) {
throw new RuntimeException(e);
}
});
System.out.println(payment);
}
Вывод вы получите примерно такой:
Payment{id='null', settledAt=2025-11-10T13:41:55.756150Z}
Я почему сейчас привёл этот пример - суть в том, что некоторые фичи самой платформы Java, на данный момент, требуют возможности мутирования final полей. Так просто исторически сложилось.
Закончить эту секцию хочу следующим: разработчики платформы Java прекрасно понимают, что сериализация была спроектирована не очень хорошо, и что сейчас от этого у платформы много проблем. Опять же, возвращаемся к мысли о том, насколько дорого стоят платформе Java "проблемные" фичи.
Теперь к делу. Что на самом деле происходит
Ну и теперь довольно короткая часть о сути изменений. На самом деле идея состоит в том, чтобы сделать простую, банальную вещь: final должен быть неизменяемый... всегда... ну, почти всегда... только если кто-то очень не попросит. Но по умолчанию менять его будет нельзя!
Это означает, что, предположительно, подобного рода трюки, что я привёл выше, будут работать на JDK 26, но будут warning-и, а в последующих версиях Java платформы код выше не будет работать из коробки.
Почему это важно вообще для Java платформы
Одна из ключевых оптимизаций языка в Java - constant folding, оно же свертывание констант. Это одна из первых оптимизаций, которая в общем и целом применяется в общей цепочке. Она во многом влияет на то, какие оптимизации далее сможет применить VM (например inlining или loop unrolling и т.п.). И сам факт того, что final поле на самом деле не final очень сильно мешает constant-фолдить константы. А это влияет на дальнейшую оптимизацию. Об этом пишут архитекторы в самом JEP 500.
Я больше того скажу, в Java довольно давно была аннотация @Stable, которая, помимо всего прочего:
В теории могла быть поставлена как над
final, так и не надfinalполемПо сути давала VM "гарантию", что значение данному аннотированному полю будет присвоено всего один раз.
Вам, как рядовым разработчикам, недоступна
Но она дает разработчикам JDK у себя в проекте сказать VM-у: "Чувак, мне верить можно, я тебе гарантирую, что
finalвот тут реальноfinal. Я вот серьёзно. Прямо отвечаю, менять не буду. Можешь констант фолдить."
И она очень давно была своего рода "затычкой" в этой бочке, когда оптимизировать код как-то всё-таки надо, но семантика final полей в Java мешает.
Заключительное слово
В общем, посмотрим, что из этого выйдет. Я очень рад тому, как Java платформа после перехода на шестимесячный релизный цикл развивается. Пожелаем ей успехов. А пока, друзья, имейте эти изменения в виду. Однозначно, что подобного рода изменения потребуют адаптации со стороны крупных фреймворков. Тем не менее, возможно, они в том числе и заденут Ваши самописные велосипеды.
Всем успехов!
Комментарии (2)

novoselov
10.11.2025 15:52наш любимый Spring так тоже умеет
В JDK21+ уже не умеет, т.к. отсутствует поле
modifiersБолее того попытка сделать то же самое через Unsafe (для private static final) может работать в Debug, но в релизе не давать никакого результата (как и ошибок). Особенно если используется простой код типа getValue() {return VALUE; }, что оптимизируется компилятором до возвращения константы и заканчивается удалением поля.
poxvuibr
Статью надо было назвать Честный final!
Слово "integrity" можно перевести на русский по разному, но в разговорной речи оно чаще всего означает "честность". А в программировании его часто переводят как целостность и обычно это хорошо отражает смысл текста. Но тут был редкий случай перевести "integrity" как честность даже в контексте нашего любимого айти.
Integrity by Default в этой статье означает, что final в коде это действительно должен быть честный final, значение которого совершенно честно нельзя изменить никак и никому. Честность по умолчанию! Я бы перевёл так, жаль возможность упущена.