Команда проекта Valhalla выпустила early-access сборку JDK с полной реализацией JEP 401 — value-классы и объекты теперь можно попробовать в действии! В новом переводе от команды Spring АйО — примеры использования, объяснение концепции, сравнение производительности с обычными объектами и практические советы для разработчиков.
Получение Early-Access сборок
Чтобы начать, перейдите на сайт jdk.java.net/valhalla и скачайте EA сборку JDK. Вы также можете ознакомиться с release notes`ами, чтобы понять, что включено в релиз.
Комментарий от Евгения Сулейманова
EA-сборки предназначены для экспериментов, API/семантика могут меняться; производственные нагрузки не рекомендуются
Чтобы начать пользоваться добавьте явные команды запуска с превью:
java --enable-preview
Распакуйте архив, поместите его в удобное место и используйте каталог bin для запуска таких команд, как java и javac. На своем Mac я установлю переменную окружения для удобного доступа к этим командам в приведённых ниже примерах:
% -> export jdk401="$PWD/jdk-26.jdk/Contents/Home/bin" % -> "$jdk401"/java --version openjdk 26-jep401ea2 2026-03-17 OpenJDK Runtime Environment (build 26-jep401ea2+1-1) OpenJDK 64-Bit Server VM (build 26-jep401ea2+1-1, mixed mode, sharing)
Эксперименты с value-объектами
Как объясняется в JEP, value-объекты — это экземпляры value-классов, которые содержат только final поля и не обладают объектной идентичностью (object identity).
Комментарий от экспертов Михаила Поливахи и Федора Сазонова
Речь о том, что оператор double equals (т.е. ==) для value объектов, конечно, работает, но по другому, отличному от обычных объектов принципу. На данный момент в англоязыной литературе используют термин "statewise equivalence", но терминология пока довольно шаткая и может меняться. Statewise equivalence декларирует, что оператор == будет проевалюирован в true в случае если:
Эти value объекты имеют один тип
Все примитивные поля этих value объектов содержат одинаковые значения
Все референсные поля при сравнении на == также евалюируются в true (т.е. идёт некая рекурсивная проверка)
Ряд классов JDK, включая Integer и LocalDate, становятся value-классами при запуске Java в режиме preview.
В JShell метод Objects.hasIdentity позволяет легко определить, какие объекты являются value-объектами, а какие — обычными identity-объектами:
% -> "$jdk401"/jshell --enable-preview | Welcome to JShell -- Version 26-jep401ea2 | For an introduction type: /help intro jshell> Objects.hasIdentity(Integer.valueOf(123)) $1 ==> false jshell> Objects.hasIdentity("abc") $2 ==> true jshell> Objects.hasIdentity(LocalDate.now()) $3 ==> false jshell> Objects.hasIdentity(new ArrayList<>()) $4 ==> true
Value-объекты во многом ведут себя так же, как и identity-объекты. Однако есть одно отличие: оператор == не может определить, являются ли два value-объекта «одним и тем же объектом» — у них просто нет identity для сравнения. Вместо этого == проверяет, эквивалентны ли объекты по состоянию: принадлежат ли они к одному классу и содержат ли одинаковые значения полей.
jshell> LocalDate d1 = LocalDate.now() d1 ==> 2025-10-23 jshell> LocalDate d2 = d1.plusDays(365) d2 ==> 2026-10-23 jshell> LocalDate d3 = d2.minusDays(365) d3 ==> 2025-10-23 jshell> d1 == d3 $8 ==> true
Эквивалентность по состоянию не заменяет хорошо продуманный метод equals, созданный автором класса.
Комментарий от Александра Шустанова
Например, два экземпляра BigDecimal с разными представлениями (new BigDecimal("1.0") и new BigDecimal("1.00")) имеют различное внутреннее состояние, но при этом логически равны.Поэтому, даже с появлением value-классов, всё ещё рекомендуется явно определять метод equals(), если у вашего типа есть особая семантика равенства, выходящая за рамки простого сравнения состояния.
В некоторых случаях два экземпляра value-класса с разными внутренними состояниями всё же должны считаться еквивалентными. Поэтому, как и прежде, рекомендуется избегать оператора == и предпочитать метод equals для сравнения объектов.
Вы можете объявлять собственные value-классы с помощью ключевого слова value. Многие record`ы хорошо подходят для преобразования в value-классы:
jshell> value record Point(int x, int y) {} | created record Point jshell> Point p = new Point(17, 3) p ==> Point[x=17, y=3] jshell> Objects.hasIdentity(p) $11 ==> false jshell> new Point(17, 3) == p $12 ==> true
Производительность value-объектов
Зачем вообще объявлять value-класс, если можно использовать обычный класс с нормальным и всем привычным поведением?
Одна и�� причин — семантическая. Если ваш класс представляет неизменяемые значения предметной области, которые взаимозаменяемы при одинаковом состоянии, то предоставление таким объектам всех признаков идентичности лишь усложняет систему без необходимости. Лучше объявить value-класс и полностью отказаться от identity.
Но самая веская причина — в том, что JVM может очень агрессивно оптимизировать value-объекты таким образом, какой невозможен для обычных объектов. Например, ссылка на value-объект не обязана указывать на уникальное место в памяти. Вместо этого состояние объекта может быть “заинлайнено” в хипе, то есть минуя непосредственно сам pointer.
Комментарий от Федора Сазонова и Михаила Поливахи
Flattening значений в хипе для value объектов визуально можно хорошо представить таким образом:

Как вы можете видеть, в комплексных объектах (например, как указано выше - в массиве), в heap-е отсутствуют ссылки как таковые. В базовом сценарии, для обычных объектов, layout бы выглядел таким образом:

Однако, и это ещё не все. Возможны и ещё более агрессивные оптимизации. Например, довольно часто, в случае, если объект является довольно компактным, HotSpot сможет не просто не аллоцировать объект непосредственно в heap (escape analysis), а ещё и раскладывать объект на регистры того или иного ядра процессора (за счет отсутствия identity и соот-но изменения семантики double equals). Это, очевидно, позволяет совершать крайне быстрые доступы к полям объекта.
Этот приём называется уплощением кучи (heap flattening) и может значительно снизить затраты на загрузку объектов из памяти.
Комментарий от Евгения Сулейманова
Уплощение - право VM, а не гарантия. Зависит от профиля, структуры памяти и null-ов.
Для теста создадим очень большой массив value-объектов LocalDate и просуммируем значения их годов. Чтобы смоделировать реалистичное распределение объектов в памяти, заполним массив из неотсортированного HashSet с объектами LocalDate. Выполним простейшее профилирование, измерив время выполнения итерации по массиву (Примечание: для более точного профилирования следует использовать JMH).
void main(String... args) { int size = 50_000_000; if (args.length > 0) size = Integer.parseInt(args[0]); LocalDate[] arr = makeArray(size); for (int i = 1; i <= 5; i++) { double t = time(() -> sumYears(arr)); IO.println("Attempt " + i + ": " + t); } } /// Expensive task to be timed long sumYears(LocalDate[] dates) { long result = 0; for (var d : dates) result += d.getYear(); return result; } /// Make an array of LocalDates, unpredictably ordered LocalDate[] makeArray(int size) { HashSet<LocalDate> set = new HashSet<>(); for (int i = 0; i < size; i++) set.add(LocalDate.ofEpochDay(i)); return set.toArray(new LocalDate[0]); } /// Run a task and report the elapsed wall-clock time in ms double time(Runnable r) { var start = Instant.now(); r.run(); var end = Instant.now(); return Duration.between(start, end).toNanos() / 1_000_000.0; }
В качестве базовой линии я поместил следующий код в файл DateTest.java и запустил его на своём MacBook Pro без включённых preview фич. Полученные результаты:
% -> "$jdk401"/java DateTest.java Attempt 1: 82.703 Attempt 2: 77.716 Attempt 3: 74.959 Attempt 4: 71.962 Attempt 5: 71.915
Когда я включаю preview-фичи, LocalDate становится value-классом, и его экземпляры могут быть напрямую уплощены в массиве. Благодаря сокращению количества загрузок из памяти JVM удаётся добиться почти трёхкратного прироста производительности:
% -> "$jdk401"/java --enable-preview DateTest.java Attempt 1: 41.959 Attempt 2: 38.992 Attempt 3: 25.466 Attempt 4: 28.404 Attempt 5: 25.027
Результаты могут варьироваться в зависимости от машины и размера массива. Однако главное здесь — использование value-объектов в вычислениях, критичных к производительности, позволяет JVM применять новые, значительные оптимизации, невозможные для объектов с identity.
Следующие шаги
Это бета-версия программного обеспечения, и в ней, безусловно, могут быть ошибки и неожиданные проблемы с производительностью.
Разумеется, простое добавление ключевого слова value в код не устранит автоматически все узкие места в производительности программы. Пользователям рекомендуется внимательно ознакомиться с JEP 401, чтобы лучше понять, какие именно оптимизации возможны, и использовать инструменты профилирования, такие как JDK Flight Recorder, чтобы увидеть, как value-объекты влияют на работу их приложений.
Мини-FAQ ("что это и зачем") от Евгения Сулейманова
Что такое value-классы/объекты простыми словами?
Это обычные классы без объектной идентичности: все поля “
final”, “==” сравнивает состояние, синхронизация/мониторы - недоступны. Их можно оптимальнее хранить/перемещать в памяти, чем identity-объекты.Зачем мне это как разработчику?
Чтобы честно моделировать "значения домена" (деньги, дата, точка, диапазон) без накладных расходов идентичности, и чтобы JVM могла применять уплотнение/уплощение и сокращать лишние разыменования (особенно в массивах/дженериках). Это дает шанс на заметный выигрыш в latency/throughput - где это уместно.
Меняется ли поведение “
==”/”equals”?Для value-объектов “
==” сравнивает поля (состояние), а у identity-объектов - как раньше, ссылки. В прикладном коде по-прежнему предпочитайте “equals”, т.к. бизнес-семантика равенства может отличаться от полного равенства полей.Можно ли просто добавить “value” "везде" и стать быстрее?
Нет. Это превью-функциональность, оптимизации - право JVM, а не контракт. Есть ограничения совместимости (мониторы, weak-refs, прокси). Профилируйте (JFR/JMH), а мигрируйте только те типы, у которых равенство-по-состоянию естественно и идентичность не нужна.
Что с экосистемой и как безопасно начинать?
- Не трогайте Entity-классы и все, что ожидает identity.
- Для DTO/встраиваемых типов - можно пробовать “record”/”value record”/value-класс.
- Проверяйте “Objects.hasIdentity”, ловите “IdentityException” в тестах.

Присоединяйтесь к русскоязычному сообществу разработчиков на Spring Boot в телеграм — Spring АйО, чтобы быть в курсе последних новостей из мира разработки на Spring Boot и всего, что с ним связано.
