Команда проекта 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 и всего, что с ним связано.
Bifurcated
А как работает присвоение null таким value классам? Помню хотели от null отказаться, но проблема, что не понятно как инициализировать
Yami-no-Ryuu
Пустой объект типа? Наверное. Похоже в HotSpot не тегированные указатели, иначе можно было помечать в младших битах.